Jump to content

Ultra Engine testing


Josh
 Share

Recommended Posts

I didn't have LunarG installed on my Win11 machine or else I would have said something as my code does this. Maybe that's a reason why repositioning the UI isn't working properly.

Also, I was getting huge performance loss when doing this but it can also be my 1050 outputting 2580x1920.

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

2 hours ago, Josh said:

Currently it's not really possible without using a pixmap, which would only work with image data in system memory, but it would probably not be too difficult to add an option for this, probably a Widget::SetTexture() method.

That would be great.

 

1 hour ago, Josh said:

This code will work, but I don't think the engine likes constantly switching back and forth between worlds. It would be much better to use render layers to control what gets drawn on which camera. There is also a Camera::SetRealtime method which can be used to draw a camera only once or intermittently.

Didn't think about render layers.  My mind was stuck in Leadwerks land.  :P

Ultra is quickly prooving really powerfull.  Cant wait to see what the next year of development brings.

Link to comment
Share on other sites

3 hours ago, Josh said:

Also, you guys should use floating points when setting the 2D camera's position:

camera2D->SetPosition(float(framebuffer->size.x) * 0.5f, float(framebuffer->size.y) * 0.5f, 0.0f);

If you do integer division your position can be slightly off if the framebuffer happens to have an odd number for width or height. This WILL cause your sprites to not be pixel-perfect, and will cause blurry font rendering.

This actually fixed a few of the 3D GUI issues of panel borders missing or not lining up.

Link to comment
Share on other sites

I'm going with render layers instead of multiple worlds.  Have found a few issues;

  • Setting a pivots render layer causes a crash in both debug and release (I know a pivot can not be rendered so there may not be a need for this to work)
  • Setting the pivot as box2's parent will cause it to not show in the set render layer.  (I assumed because the pivot was on a different render layer?)
  • Box2 renders on render layer 2 but it's shadow still shows on render layer 0
  • And box2 shows some sort of shadowing from the floor box on render layer 0
  • I don't think lights are working between the layers because of the shadow issues and models on layer 2 still receive light from the directional light on layer 0.  I've also made a spotlight in another project on render layer 2 and it doesn't seem to show up, but it's hard to tell due the shadowing and light from layer 0.

RenderLayerIssues.thumb.png.28ce57278e3403c098b6ed0d01e430ce.png

 

#include "UltraEngine.h"
#include "ComponentSystem.h"


using namespace UltraEngine;


int main(int argc, const char* argv[])
{
    auto displays = GetDisplays();
    auto window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
    auto world = CreateWorld();

    auto framebuffer = CreateFramebuffer(window);

    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetFov(70);
    camera->SetPosition(0, 2, -3);

    auto font = LoadFont("Fonts/arial.ttf");
    auto ui = CreateInterface(world, font, framebuffer->size);
    ui->SetRenderLayers(RENDERLAYER_1);
    ui->root->SetColor(0, 0, 0, 0);

    auto ui_cam = CreateCamera(world, PROJECTION_ORTHOGRAPHIC);
    ui_cam->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);
    ui_cam->SetRenderLayers(RENDERLAYER_1);
    ui_cam->SetClearMode(CLEAR_DEPTH);


    auto tex_buffer = CreateTextureBuffer(256, 256);
    auto cam2 = CreateCamera(world);
    cam2->SetRenderLayers(RENDERLAYER_2);
    cam2->SetClearColor(1, 0, 0);
    cam2->SetRenderTarget(tex_buffer);
    cam2->Move(0, 0, -5);
    auto light2 = CreateDirectionalLight(world);

    auto pivot = CreatePivot(world);
    //pivot->SetRenderLayers(RENDERLAYER_2);//Causes a crash

    auto box2 = CreateBox(world);
   // box2->SetParent(pivot);//box2 will not render
    box2->SetRenderLayers(RENDERLAYER_2);

    auto mat = CreateMaterial();
    mat->SetTexture(tex_buffer->GetColorAttachment());

    auto sprite = CreateSprite(world, 256, 256);
    sprite->SetRenderLayers(RENDERLAYER_1);
    sprite->SetMaterial(mat);


    auto light = CreateDirectionalLight(world);
    light->SetRotation(35, 45, 0);
    light->SetRange(-10, 10);

    auto floor = CreateBox(world, 1000.0f, 0.1f, 1000.0f);

    while (window->Closed() == false and window->KeyDown(KEY_ESCAPE) == false)
    {
        box2->Turn(0, 1, 0);

        world->Update();
        world->Render(framebuffer);
    }
    return 0;
}

 

  • Thanks 1
Link to comment
Share on other sites

4 hours ago, Josh said:

Update

Currently it's not really possible without using a pixmap, which would only work with image data in system memory, but it would probably not be too difficult to add an option for this, probably a Widget::SetTexture() method.

What do you propose? Something like Camera::SetClipPlane(const Plane& p, const int index)?

Yes, I know the method is in the camera class but commented out. as I think it would require changes to the vertex shaders, like in Leadwerks. But it would be nice to have a way to cut out everything by plane. Also, it would make water refraction possible, when the global refraction is no option (don’t know when, just mentioning it for completeness ;) )

  • Intel® Core™ i7-8550U @ 1.80 Ghz 
  • 16GB RAM 
  • INTEL UHD Graphics 620
  • Windows 10 Pro 64-Bit-Version
Link to comment
Share on other sites

10 hours ago, Josh said:

You have too many events piling up in the queue faster than you are processing them. Change "if PeekEvent" to "while PeekEvent()".

Even with that fix, it's still not drawing correctly after resizing the window. It might have to do with the validation error you're getting, but not sure.

image.thumb.png.f046d132e0a5a71c4b8eb601e12dd344.png

#include "UltraEngine.h"
#include "ComponentSystem.h"

using namespace UltraEngine;

int main(int argc, const char* argv[])
{
    //Get the displays
    auto displays = GetDisplays();

    //Create a window
    auto window = CreateWindow("Ultra Engine - Normal", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);

    //Create a world
    auto world = CreateWorld();

    //Create a framebuffer
    auto framebuffer = CreateFramebuffer(window);

    //Create a camera
    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetFov(70);
    camera->SetPosition(0, 0, -3);
    camera->SetViewport(200, 0, framebuffer->size.x - 200, framebuffer->size.y);

    auto uiCamera = CreateCamera(world, PROJECTION_ORTHOGRAPHIC);
    uiCamera->SetRenderLayers(RENDERLAYER_1);
    uiCamera->SetClearMode(CLEAR_DEPTH);
    uiCamera->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);
    auto ui = CreateInterface(world, LoadFont("Fonts/arial.ttf"), iVec2(200, framebuffer->size.y));
    ui->SetRenderLayers(RENDERLAYER_1);

    auto sz = ui->root->ClientSize();
    auto listbox = CreateListBox(5, 5, sz.x - 10, 200, ui->root, LISTBOX_DEFAULT)->As<ListBox>();
    auto tabber = CreateTabber(5, 205, sz.x - 10, sz.y - 205, ui->root)->As<Tabber>();
    tabber->AddItem("Settings", true);
    tabber->AddItem("Output");

    for (int i = 0; i < 100; i++)
    {
        listbox->AddItem("Item " + String(i));
    }

    //Create a light
    auto light = CreateBoxLight(world);
    light->SetRotation(35, 45, 0);
    light->SetRange(-10, 10);

    //Create a box
    auto box = CreateBox(world);
    box->SetColor(0, 0, 1);

    //Entity component system
    auto actor = CreateActor(box);
    auto component = actor->AddComponent<Mover>();
    component->rotation.y = 45;

    //Main loop
    while (window->Closed() == false and window->KeyDown(KEY_ESCAPE) == false)
    {
        while (PeekEvent())
        {
            auto ev = WaitEvent();
            ui->ProcessEvent(ev);

            switch (ev.id)
            {
            case UltraEngine::EVENT_WINDOWCLOSE:
                if (ev.source == window) exit(0);
                break;

            default:
                break;
            }
        }

        // Rebuild the window.
        if (window->KeyDown(KEY_SPACE))
        {
            static bool bSwapped = false;
            framebuffer = NULL;
            window = NULL;

            if (!bSwapped)
            {
                // Make it a tad bigger
                window = CreateWindow("Ultra Engine - Resized", 0, 0, 1400, 800, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
                framebuffer = CreateFramebuffer(window);
            }
            else
            {
                window = CreateWindow("Ultra Engine - Normal", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
                framebuffer = CreateFramebuffer(window);
            }

            // Reposition the camera.
            uiCamera->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);

            // Resize the ui
            ui->Redraw(0, 0, framebuffer->size.x, framebuffer->size.y);
            ui->UpdateLayout();

            bSwapped = !bSwapped;
        }

        world->Update();
        if (framebuffer) world->Render(framebuffer);
    }
    return 0;
}

 

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

This example demonstrates that camera creation order matters when I feel like it shouldn't.

Comment/Uncomment the macro to toggle.

#include "UltraEngine.h"
#include "ComponentSystem.h"

using namespace UltraEngine;

// Toggle me to change the order.
// Creation order of the cameras should not matter.
//#define DRAW_2DCAM_FIRST

int main(int argc, const char* argv[])
{
    //Get the displays
    auto displays = GetDisplays();

    //Create a window
    auto window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);

    //Create framebuffer
    auto framebuffer = CreateFramebuffer(window);

    //Create world
    auto world = CreateWorld();

#ifdef DRAW_2DCAM_FIRST
    //Create a ortho camera first
    auto camera2D = CreateCamera(world, UltraEngine::PROJECTION_ORTHOGRAPHIC);
    camera2D->SetDepthPrepass(false);
    camera2D->SetClearMode(UltraEngine::CLEAR_DEPTH);
    camera2D->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    camera2D->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);

    //Create sprite
    auto sprite = LoadSprite(world, "https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Materials/Sprites/nightraider.dds");
    sprite->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    sprite->SetPosition(0, 0);
    sprite->mesh->material->SetAlphaMask(true);
#endif

    //Create the 3D camera
    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetFov(70);
    camera->SetPosition(0, 0, -3);

#ifndef DRAW_2DCAM_FIRST
    //Create a ortho camera first
    auto camera2D = CreateCamera(world, UltraEngine::PROJECTION_ORTHOGRAPHIC);
    camera2D->SetDepthPrepass(false);
    camera2D->SetClearMode(UltraEngine::CLEAR_DEPTH);
    camera2D->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    camera2D->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);

    //Create sprite
    auto sprite = LoadSprite(world, "https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Materials/Sprites/nightraider.dds");
    sprite->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    sprite->SetPosition(0, 0);
    sprite->mesh->material->SetAlphaMask(true);
#endif

    // Create a box for us to see
    auto model = CreateBox(world);
    model->SetColor(1, 0, 0);
    model->Turn(45, 45, 0);

    //Main loop
    while (window->Closed() == false and window->KeyHit(KEY_ESCAPE) == false)
    {
        world->Update();
        world->Render(framebuffer);
    }
    return 0;
}

 

  • Upvote 1

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

Today I found this. What's the difference between these two? 

	extern WString AppDir();
	extern WString AppPath();

Most things seem to be working wonderfully in my complex application. One thing that caught me off guard was that the black panel (Which is a sprite) needed some code to work correctly. I thought I could use SetColor to change its alpha, I figured out that I needed to load a material with the unlit shader family first, then tell the material to allow transparency.  

image.thumb.png.fa0882d691106750cd9258c1a27baa15.png

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

3 hours ago, reepblue said:

Today I found this. What's the difference between these two? 

AppPath() is the full path including the executable name. AppDir is the folder part of that. It looks like this is missing from the docs.

  • Thanks 1

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

9 minutes ago, SpiderPig said:

Got it working thanks.  What about setting an entirely different font per widget?

This is not currently supported.

  • Thanks 1

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

16 hours ago, reepblue said:

This example demonstrates that camera creation order matters when I feel like it shouldn't.

Comment/Uncomment the macro to toggle.

I fixed your example:

#include "UltraEngine.h"
#include "ComponentSystem.h"

using namespace UltraEngine;

// Toggle me to change the order.
// Creation order of the cameras should not matter.
//#define DRAW_2DCAM_FIRST

int main(int argc, const char* argv[])
{
    //Get the displays
    auto displays = GetDisplays();

    //Create a window
    auto window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);

    //Create framebuffer
    auto framebuffer = CreateFramebuffer(window);

    //Create world
    auto world = CreateWorld();

#ifdef DRAW_2DCAM_FIRST
    //Create a ortho camera first
    auto camera2D = CreateCamera(world, UltraEngine::PROJECTION_ORTHOGRAPHIC);
    camera2D->SetDepthPrepass(false);
    camera2D->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    camera2D->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);
    camera2D->SetClearColor(0.125);

    //Create sprite
    auto sprite = LoadSprite(world, "https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Materials/Sprites/nightraider.dds");
    sprite->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    sprite->SetPosition(0, 0);
    sprite->mesh->material->SetAlphaMask(true);
#endif

    //Create the 3D camera
    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetFov(70);
    camera->SetPosition(0, 0, -3);
#ifdef DRAW_2DCAM_FIRST
    camera->SetClearMode(CLEAR_DEPTH);
#endif

#ifndef DRAW_2DCAM_FIRST
    //Create a ortho camera first
    auto camera2D = CreateCamera(world, UltraEngine::PROJECTION_ORTHOGRAPHIC);
    camera2D->SetDepthPrepass(false);
    camera2D->SetClearMode(UltraEngine::CLEAR_DEPTH);
    camera2D->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    camera2D->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);

    //Create sprite
    auto sprite = LoadSprite(world, "https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Materials/Sprites/nightraider.dds");
    sprite->SetRenderLayers(UltraEngine::RENDERLAYER_7);
    sprite->SetPosition(0, 0);
    sprite->mesh->material->SetAlphaMask(true);
#endif

    // Create a box for us to see
    auto model = CreateBox(world);
    model->SetColor(1, 0, 0);
    model->Turn(45, 45, 0);

    //Main loop
    while (window->Closed() == false and window->KeyHit(KEY_ESCAPE) == false)
    {
        world->Update();
        world->Render(framebuffer);
    }
    return 0;
}

 

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

It took me forever to spot but I guess the cameras have to has matching clear colors? I probably wouldn't ever have known that. But now it kind of makes sense. I'll try it tonight, but I might just set all my camera clear colors to 0.

I also plan on setting up a git repo with all my examples so people can refer to in the future.

  • Like 2

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

The problem in your code was that when the ortho camera was created first, the 3D camera did not have its clear mode set to CLEAR_DEPTH, so it was still clearing the color after the ortho camera was drawn.

The other issue was that the ortho camera was not clearing the color when it was the first created camera, so the contents of the previous frame would still be visible, causing a "hall of mirror" effect if the camera moves.

Clear color has no effect if the clear mode does not include CLEAR_COLOR.

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

Hmm, ok. I'll take a look. I kind of want to make a system that'll allow Huds and such on to be created and released in between layer 0 and the UI and debug layers. This way a player or weapon component can manage it's own hud independently from everything else. Since I know the order of creation doesn't matter if the settings are correct, should be doable.

Nothing you need to do, this is my riddle to solve. 

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

18 hours ago, reepblue said:

Even with that fix, it's still not drawing correctly after resizing the window. It might have to do with the validation error you're getting, but not sure.

This will take some time to figure out what the correct behavior here should be.

If you are making a desktop application with a 3D viewport, it's better to create the interface directly on the window, and use a child window to make an embedded 3D viewport:
https://github.com/UltraEngine/Documentation/blob/master/CPP/Leadwerks.md#advanced-example

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

14 minutes ago, Josh said:

This will take some time to figure out what the correct behavior here should be.

If you are making a desktop application with a 3D viewport, it's better to create the interface directly on the window, and use a child window to make an embedded 3D viewport:
https://github.com/UltraEngine/Documentation/blob/master/CPP/Leadwerks.md#advanced-example

Yeah, I dug up the example on this thread and updated the code and that still works fine.

Looking forward to this being resolved. Resizing the UI is why I have players with Cyclone to restart the game when they change their window settings, and I don't wish to have that be the case going forward.

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

Update

Audio filters are stored in JSON files. There are about a dozen different types of effects. One type of effect is EAXReverb, which includes all the classic EAX reverb presets (over 100), but there are other effects like distortion, auto-wah, flanger, chorus, and more. At this time, EAXReverb effects are the only ones that support loading parameters from their files. Other effects will always use their default settings, for now. A speaker can use multiple audio filters, like guitar effects pedals in a chain.

This is all it takes:

auto filter = LoadAudioFilter("https://raw.githubusercontent.com/UltraEngine/Assets/main/Sound/Filters/EAXReverb/SewerPipe.json");
speaker->SetFilter(filter);

You can read more about head-related transfer function here: https://en.wikipedia.org/wiki/Head-related_transfer_function

  • Like 1
  • Upvote 1

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

Update

#include "UltraEngine.h"
#include "ComponentSystem.h"

using namespace UltraEngine;

int main(int argc, const char* argv[])
{
    //Get the displays
    auto displays = GetDisplays();

    //Create a window
    auto window = CreateWindow("Ultra Engine - Normal", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);

    //Create a world
    auto world = CreateWorld();

    //Create a framebuffer
    auto framebuffer = CreateFramebuffer(window);

    //Create a camera
    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetFov(70);
    camera->SetPosition(0, 0, -3);
    camera->SetViewport(200, 0, framebuffer->size.x - 200, framebuffer->size.y);

    auto uiCamera = CreateCamera(world, PROJECTION_ORTHOGRAPHIC);
    uiCamera->SetRenderLayers(RENDERLAYER_1);
    uiCamera->SetClearMode(CLEAR_DEPTH);
    uiCamera->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);
    auto ui = CreateInterface(world, LoadFont("Fonts/arial.ttf"), iVec2(200, framebuffer->size.y));
    ui->SetRenderLayers(RENDERLAYER_1);

    auto sz = ui->root->ClientSize();
    auto listbox = CreateListBox(5, 5, sz.x - 10, 200, ui->root, LISTBOX_DEFAULT)->As<ListBox>();
    auto tabber = CreateTabber(5, 205, sz.x - 10, sz.y - 205, ui->root)->As<Tabber>();
    tabber->AddItem("Settings", true);
    tabber->AddItem("Output");
    tabber->SetLayout(1, 0, 1, 1);

    for (int i = 0; i < 100; i++)
    {
        listbox->AddItem("Item " + String(i));
    }

    //Create a light
    auto light = CreateBoxLight(world);
    light->SetRotation(35, 45, 0);
    light->SetRange(-10, 10);

    //Create a box
    auto box = CreateBox(world);
    box->SetColor(0, 0, 1);

    //Entity component system
    auto actor = CreateActor(box);
    auto component = actor->AddComponent<Mover>();
    component->rotation.y = 45;

    //Main loop
    while (window->Closed() == false and window->KeyDown(KEY_ESCAPE) == false)
    {
        while (PeekEvent())
        {
            auto ev = WaitEvent();
            ui->ProcessEvent(ev);

            switch (ev.id)
            {
            case UltraEngine::EVENT_WINDOWCLOSE:
                if (ev.source == window) exit(0);
                break;

            default:
                break;
            }
        }

        // Rebuild the window.
        if (window->KeyDown(KEY_SPACE))
        {
            static bool bSwapped = false;
            framebuffer = NULL;
            window = NULL;

            if (!bSwapped)
            {
                // Make it a tad bigger
                window = CreateWindow("Ultra Engine - Resized", 0, 0, 1400, 800, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
                framebuffer = CreateFramebuffer(window);
            }
            else
            {
                window = CreateWindow("Ultra Engine - Normal", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
                framebuffer = CreateFramebuffer(window);
            }

            // Reposition the camera.
            uiCamera->SetPosition((float)framebuffer->size.x * 0.5f, (float)framebuffer->size.y * 0.5f, 0);
            
            //Resize the interface
            ui->SetSize(iVec2(200, framebuffer->size.y));

            bSwapped = !bSwapped;
        }

        world->Update();
        if (framebuffer) world->Render(framebuffer);
    }
    return 0;
}

 

  • Like 1

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

  • Josh locked this topic
Guest
This topic is now closed to further replies.
 Share

×
×
  • Create New...