Skip to content

Commit

Permalink
Add Statics Window and interaction with statics in 3D view (#1240)
Browse files Browse the repository at this point in the history
Add Statics Window. Can be created using Ctrl + S or via the menu.
Add more interaction with statics into the viewer - context menu hiding, clicking.
Closes #912
  • Loading branch information
chreden authored May 26, 2024
1 parent eb3505a commit 68a3a3f
Show file tree
Hide file tree
Showing 60 changed files with 1,356 additions and 51 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ CTRL + M | New Rooms window
CTRL + L | New Lights window
CTRL + K | New Camera/Sink window
CTRL + P | New Plugins window
CTRL + S | New Statics Window
CTRL + H | Toggle room lighting
F1 | Toggle settings window
F5 | Reload current level
Expand Down
2 changes: 2 additions & 0 deletions doc/lua/staticmesh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

| Name | Type | Mode | Description |
| ---- | ---- | ---- | ---- |
| breakable | boolean | R | Whether this is a breakable static |
| collision | [BoundingBox](boundingbox.md) | R | Collision bounding box |
| id | number | R | Mesh ID or sprite texture ID |
| position | [Vector3](vector3.md) | R | Static mesh position in the world. |
| room | [Room](room.md) | R | Room that contains the static mesh |
| rotation | number | R | Static mesh rotation |
| type | string | R | Type of static mesh (Mesh/Sprite) |
| visible| boolean | RW | Whether the item is visible in the viewer |
| visibility | [BoundingBox](boundingbox.md) | R | Visibility bounding box |
23 changes: 22 additions & 1 deletion trview.app.tests/ApplicationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#include <trview.app/Mocks/Plugins/IPlugins.h>
#include <trview.app/Mocks/Windows/IPluginsWindowManager.h>
#include <trview.app/Mocks/UI/IFonts.h>
#include <trview.app/Mocks/Windows/IStaticsWindow.h>
#include <trview.app/Mocks/Windows/IStaticsWindowManager.h>

using namespace trview;
using namespace trview::tests;
Expand Down Expand Up @@ -87,6 +89,7 @@ namespace
std::unique_ptr<IPluginsWindowManager> plugins_window_manager{ mock_unique<MockPluginsWindowManager>() };
IRandomizerRoute::Source randomizer_route_source { [](auto&&...) { return mock_shared<MockRandomizerRoute>(); } };
std::shared_ptr<IFonts> fonts { mock_shared<MockFonts>() };
std::unique_ptr<IStaticsWindowManager> statics_window_manager{ mock_unique<MockStaticsWindowManager>() };

std::unique_ptr<Application> build()
{
Expand All @@ -96,7 +99,7 @@ namespace
std::move(items_window_manager), std::move(triggers_window_manager), std::move(route_window_manager), std::move(rooms_window_manager),
level_source, startup_options, dialogs, files, std::move(imgui_backend), std::move(lights_window_manager), std::move(log_window_manager),
std::move(textures_window_manager), std::move(camera_sink_window_manager), std::move(console_manager),
plugins, std::move(plugins_window_manager), randomizer_route_source, fonts);
plugins, std::move(plugins_window_manager), randomizer_route_source, fonts, std::move(statics_window_manager));
}

test_module& with_dialogs(std::shared_ptr<IDialogs> dialogs)
Expand Down Expand Up @@ -237,6 +240,12 @@ namespace
return *this;
}

test_module& with_statics_window_manager(std::unique_ptr<IStaticsWindowManager> statics_window_manager)
{
this->statics_window_manager = std::move(statics_window_manager);
return *this;
}

test_module& with_fonts(std::shared_ptr<IFonts> fonts)
{
this->fonts = fonts;
Expand Down Expand Up @@ -310,6 +319,7 @@ TEST(Application, WindowContentsResetBeforeViewerLoaded)
auto [lights_window_manager_ptr, lights_window_manager] = create_mock<MockLightsWindowManager>();
auto [textures_window_manager_ptr, textures_window_manager] = create_mock<MockTexturesWindowManager>();
auto [camera_sink_window_manager_ptr, camera_sink_window_manager] = create_mock<MockCameraSinkWindowManager>();
auto [statics_window_manager_ptr, statics_window_manager] = create_mock<MockStaticsWindowManager>();
auto route = mock_shared<MockRoute>();

std::vector<std::string> events;
Expand All @@ -330,6 +340,7 @@ TEST(Application, WindowContentsResetBeforeViewerLoaded)
EXPECT_CALL(route_window_manager, set_route(A<const std::weak_ptr<IRoute>&>())).Times(3).WillRepeatedly([&](auto) { events.push_back("route_route"); });
EXPECT_CALL(lights_window_manager, set_lights(A<const std::vector<std::weak_ptr<ILight>>&>())).Times(1).WillOnce([&](auto) { events.push_back("lights_lights"); });
EXPECT_CALL(camera_sink_window_manager, set_camera_sinks).Times(1).WillOnce([&](auto) { events.push_back("camera_sinks_camera_sinks"); });
EXPECT_CALL(statics_window_manager, set_statics).Times(1).WillOnce([&](auto) { events.push_back("statics_statics"); });
EXPECT_CALL(*route, clear()).Times(1).WillOnce([&] { events.push_back("route_clear"); });
EXPECT_CALL(*route, set_unsaved(false)).Times(1);
EXPECT_CALL(textures_window_manager, set_texture_storage).Times(1).WillOnce([&](auto) { events.push_back("textures"); });
Expand All @@ -346,6 +357,7 @@ TEST(Application, WindowContentsResetBeforeViewerLoaded)
.with_lights_window_manager(std::move(lights_window_manager_ptr))
.with_textures_window_manager(std::move(textures_window_manager_ptr))
.with_camera_sink_window_manager(std::move(camera_sink_window_manager_ptr))
.with_statics_window_manager(std::move(statics_window_manager_ptr))
.build();
application->open("test_path.tr2", ILevel::OpenMode::Full);

Expand Down Expand Up @@ -525,13 +537,16 @@ TEST(Application, WindowManagersUpdated)
EXPECT_CALL(triggers_window_manager, update).Times(1);
auto [lights_window_manager_ptr, lights_window_manager] = create_mock<MockLightsWindowManager>();
EXPECT_CALL(lights_window_manager, update).Times(1);
auto [statics_window_manager_ptr, statics_window_manager] = create_mock<MockStaticsWindowManager>();
EXPECT_CALL(statics_window_manager, update).Times(1);

auto application = register_test_module()
.with_route_window_manager(std::move(route_window_manager_ptr))
.with_items_window_manager(std::move(items_window_manager_ptr))
.with_rooms_window_manager(std::move(rooms_window_manager_ptr))
.with_triggers_window_manager(std::move(triggers_window_manager_ptr))
.with_lights_window_manager(std::move(lights_window_manager_ptr))
.with_statics_window_manager(std::move(statics_window_manager_ptr))
.build();
application->render();
}
Expand All @@ -558,6 +573,8 @@ TEST(Application, WindowManagersAndViewerRendered)
EXPECT_CALL(console_manager, render).Times(1);
auto [plugins_window_manager_ptr, plugins_window_manager] = create_mock<MockPluginsWindowManager>();
EXPECT_CALL(plugins_window_manager, render).Times(1);
auto [statics_window_manager_ptr, statics_window_manager] = create_mock<MockStaticsWindowManager>();
EXPECT_CALL(statics_window_manager, render).Times(1);
auto plugins = mock_shared<MockPlugins>();
EXPECT_CALL(*plugins, render_ui).Times(1);

Expand All @@ -575,6 +592,7 @@ TEST(Application, WindowManagersAndViewerRendered)
.with_camera_sink_window_manager(std::move(camera_sink_window_manager_ptr))
.with_console_manager(std::move(console_manager_ptr))
.with_plugins_window_manager(std::move(plugins_window_manager_ptr))
.with_statics_window_manager(std::move(statics_window_manager_ptr))
.with_viewer(std::move(viewer_ptr))
.with_plugins(plugins)
.build();
Expand Down Expand Up @@ -1307,14 +1325,17 @@ TEST(Application, OnStaticMeshSelected)
auto level = mock_shared<trview::mocks::MockLevel>();
auto static_mesh = mock_shared<MockStaticMesh>();
auto [rooms_window_manager_ptr, rooms_window_manager] = create_mock<MockRoomsWindowManager>();
auto [statics_window_manager_ptr, statics_window_manager] = create_mock<MockStaticsWindowManager>();
auto [viewer_ptr, viewer] = create_mock<MockViewer>();
auto application = register_test_module()
.with_rooms_window_manager(std::move(rooms_window_manager_ptr))
.with_statics_window_manager(std::move(statics_window_manager_ptr))
.with_viewer(std::move(viewer_ptr))
.build();

application->set_current_level(level, ILevel::OpenMode::Full, false);

EXPECT_CALL(viewer, select_static_mesh).Times(1);
EXPECT_CALL(statics_window_manager, select_static).Times(1);
rooms_window_manager.on_static_mesh_selected(static_mesh);
}
26 changes: 25 additions & 1 deletion trview.app.tests/Elements/LevelTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -829,4 +829,28 @@ TEST(Level, SkidooGenerated)
ASSERT_EQ(entities.size(), 2);
ASSERT_EQ(entities[0].TypeID, 52);
ASSERT_EQ(entities[1].TypeID, 51);
}
}

TEST(Level, StaticMeshChangingRaisesLevelChangedEvent)
{
auto static_mesh = mock_shared<MockStaticMesh>();

auto room = mock_shared<MockRoom>();
ON_CALL(*room, static_meshes).WillByDefault(Return(std::vector<std::weak_ptr<IStaticMesh>>{ static_mesh }));

auto [mock_level_ptr, mock_level] = create_mock<trlevel::mocks::MockLevel>();
EXPECT_CALL(mock_level, num_rooms()).WillRepeatedly(Return(1));

auto level = register_test_module().with_level(std::move(mock_level_ptr))
.with_room_source(
[&](auto&&...)
{
return room;
}).build();

uint32_t times_called = 0;
auto token = level->on_level_changed += [&](auto&&...) { ++times_called; };

static_mesh->on_changed();
ASSERT_EQ(times_called, 1u);
}
36 changes: 35 additions & 1 deletion trview.app.tests/Elements/RoomTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,38 @@ TEST(Room, Sector)
sector = room->sector(2, 2);
s = sector.lock();
ASSERT_FALSE(s);
}
}

TEST(Room, PickTestsStaticMesh)
{
using namespace DirectX;
using namespace DirectX::SimpleMath;

auto level = mock_shared<trlevel::mocks::MockLevel>();
ON_CALL(*level, get_static_mesh).WillByDefault(testing::Return(trlevel::tr_staticmesh{}));

trlevel::tr3_room level_room{ .static_meshes = { {} } };

auto static_mesh = mock_shared<MockStaticMesh>();
ON_CALL(*static_mesh, number).WillByDefault(Return(10));
ON_CALL(*static_mesh, visible).WillByDefault(Return(true));
EXPECT_CALL(*static_mesh, pick).Times(1).WillOnce(Return(PickResult{ true, 0, {}, {}, PickResult::Type::StaticMesh, 10 }));

auto static_mesh_source = [&](auto&&...)
{
return static_mesh;
};

auto room = register_test_module()
.with_room(level_room)
.with_static_mesh_source(static_mesh_source)
.with_tr_level(level)
.build();

auto results = room->pick(Vector3(0, 0, -2), Vector3(0, 0, 1), PickFilter::StaticMeshes);
ASSERT_EQ(results.size(), 1);
auto result = results.front();
ASSERT_EQ(result.hit, true);
ASSERT_EQ(result.type, PickResult::Type::StaticMesh);
ASSERT_EQ(result.index, 10);
}
9 changes: 9 additions & 0 deletions trview.app.tests/Elements/StaticMeshTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ TEST(StaticMesh, BoundingBoxRendered)
StaticMesh mesh({}, {}, actual_mesh, {}, bounding_mesh);
mesh.render_bounding_box(NiceMock<MockCamera>{}, NiceMock<MockLevelTextureStorage>{}, Colour::White);
}

TEST(StaticMesh, OnChangedRaised)
{
StaticMesh mesh({}, {}, mock_shared<MockMesh>(), {}, mock_shared<MockMesh>());
bool raised = false;
auto token = mesh.on_changed += [&] (){ raised = true; };
mesh.set_visible(false);
ASSERT_EQ(raised, true);
}
40 changes: 40 additions & 0 deletions trview.app.tests/Lua/Elements/Lua_StaticMeshTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ using namespace trview::tests;
using namespace testing;
using namespace DirectX::SimpleMath;

TEST(Lua_StaticMesh, Breakable)
{
auto mesh = mock_shared<MockStaticMesh>();
EXPECT_CALL(*mesh, breakable).WillOnce(Return(true));

LuaState L;
lua::create_static_mesh(L, mesh);
lua_setglobal(L, "s");

ASSERT_EQ(0, luaL_dostring(L, "return s.breakable"));
ASSERT_EQ(LUA_TBOOLEAN, lua_type(L, -1));
ASSERT_EQ(true, lua_toboolean(L, -1));
}

TEST(Lua_StaticMesh, Collision)
{
auto static_mesh = mock_shared<MockStaticMesh>();
Expand Down Expand Up @@ -115,6 +129,18 @@ TEST(Lua_StaticMesh, Rotation)
ASSERT_EQ(123.0f, lua_tonumber(L, -1));
}

TEST(Lua_StaticMesh, SetVisible)
{
auto static_mesh = mock_shared<MockStaticMesh>()->with_number(100);
EXPECT_CALL(*static_mesh, set_visible(true)).Times(1);

LuaState L;
lua::create_static_mesh(L, static_mesh);
lua_setglobal(L, "s");

ASSERT_EQ(0, luaL_dostring(L, "s.visible = true"));
}

TEST(Lua_StaticMesh, Type)
{
auto static_mesh = mock_shared<MockStaticMesh>();
Expand All @@ -134,6 +160,20 @@ TEST(Lua_StaticMesh, Type)
ASSERT_STREQ("Sprite", lua_tostring(L, -1));
}

TEST(Lua_StaticMesh, Visible)
{
auto static_mesh = mock_shared<MockStaticMesh>();
EXPECT_CALL(*static_mesh, visible).WillOnce(Return(true));

LuaState L;
lua::create_static_mesh(L, static_mesh);
lua_setglobal(L, "s");

ASSERT_EQ(0, luaL_dostring(L, "return s.visible"));
ASSERT_EQ(LUA_TBOOLEAN, lua_type(L, -1));
ASSERT_EQ(true, lua_toboolean(L, -1));
}

TEST(Lua_StaticMesh, Visibility)
{
auto static_mesh = mock_shared<MockStaticMesh>();
Expand Down
2 changes: 1 addition & 1 deletion trview.app.tests/PluginsWindowManagerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ TEST(PluginsWindowManager, RendersAllWindows)
manager->render();
}

TEST(PluginsWindowManager, CreateLightsWindowKeyboardShortcut)
TEST(PluginsWindowManager, CreatePluginsWindowKeyboardShortcut)
{
auto shortcuts = mock_shared<MockShortcuts>();
EXPECT_CALL(*shortcuts, add_shortcut(true, 'P')).Times(1).WillOnce([&](auto, auto) -> Event<>&{ return shortcut_handler; });
Expand Down
25 changes: 25 additions & 0 deletions trview.app.tests/Settings/SettingsLoaderTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,31 @@ TEST(SettingsLoader, ItemsStartupSaved)
EXPECT_THAT(output, HasSubstr("\"itemsstartup\":true"));
}

TEST(SettingsLoader, StaticsStartupLoaded)
{
auto loader = setup_setting("{\"statics_startup\":false}");
auto settings = loader->load_user_settings();
ASSERT_EQ(settings.statics_startup, false);

auto loader_true = setup_setting("{\"statics_startup\":true}");
auto settings_true = loader_true->load_user_settings();
ASSERT_EQ(settings_true.statics_startup, true);
}

TEST(SettingsLoader, StaticsStartupSaved)
{
std::string output;
auto loader = setup_save_setting(output);
UserSettings settings;
settings.statics_startup = false;
loader->save_user_settings(settings);
EXPECT_THAT(output, HasSubstr("\"statics_startup\":false"));

settings.statics_startup = true;
loader->save_user_settings(settings);
EXPECT_THAT(output, HasSubstr("\"statics_startup\":true"));
}

TEST(SettingsLoader, TriggersStartupLoaded)
{
auto loader = setup_setting("{\"triggerstartup\":false}");
Expand Down
28 changes: 28 additions & 0 deletions trview.app.tests/UI/ViewerUITests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,32 @@ TEST(ViewerUI, FontForwarded)
ASSERT_EQ(raised->second.size, 100);
}

TEST(ViewerUI, OnStaticsStartupEventRaised)
{
auto [settings_window_ptr, settings_window] = create_mock<MockSettingsWindow>();
auto ui = register_test_module().with_settings_window(std::move(settings_window_ptr)).build();

std::optional<UserSettings> settings;
auto token = ui->on_settings += [&](auto raised)
{
settings = raised;
};

settings_window.on_statics_startup(true);

ASSERT_TRUE(settings);
ASSERT_TRUE(settings.value().statics_startup);
}

TEST(ViewerUI, SetStaticsStartupUpdatesSettingsWindow)
{
auto [settings_window_ptr, settings_window] = create_mock<MockSettingsWindow>();
EXPECT_CALL(settings_window, set_statics_startup(true)).Times(1);

auto ui = register_test_module().with_settings_window(std::move(settings_window_ptr)).build();

UserSettings settings{};
settings.statics_startup = true;
ui->set_settings(settings);
}

Loading

0 comments on commit 68a3a3f

Please sign in to comment.