/
Mr.Stalin
/
FOnline-Engine
Обзор
Документация
Войти
/
Mr.Stalin
/
FOnline-Engine
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Source/Tools/Mapper.cpp
7 315 строк
256 KB
cvet
Refactor variable declarations to remove 'const' where unnecessary
11 авг 2026, 10:25
11 авг 2026, 10:25
2a2363c
Код
Авторство
О чём код?
// __________ ___ ______ _ // / ____/ __ \____ / (_)___ ___ / ____/___ ____ _(_)___ ___ // / /_ / / / / __ \/ / / __ \/ _ \ / __/ / __ \/ __ `/ / __ \/ _ ` // / __/ / /_/ / / / / / / / / / __/ / /___/ / / / /_/ / / / / / __/ // /_/ \____/_/ /_/_/_/_/ /_/\___/ /_____/_/ /_/\__, /_/_/ /_/\___/ // /____/ // FOnline Engine // https://fonline.ru // https://github.com/cvet/fonline // // MIT License // // Copyright (c) 2006 - 2026, Anton Tsvetinskiy aka cvet <cvet@tut.by> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #include "Mapper.h" #include "AngelScriptScripting.h" #include "AnyData.h" #include "ConfigFile.h" #include "DefaultSprites.h" #include "ImGuiStuff.h" #include "MetadataRegistration.h" #include "ModelSprites.h" #include "ParticleSprites.h" FO_BEGIN_NAMESPACE static constexpr ipos32 MAPPER_CONSOLE_WINDOW_OFFSET = {0, 6}; static constexpr string_view MAPPER_IMGUI_SETTINGS_KEY = "ImGuiLayout"; static constexpr int32_t DAY_TIME_WRAP_MINUTES = 1440; static constexpr int32_t DAY_TIME_VISIBLE_UPPER_BOUND = DAY_TIME_WRAP_MINUTES * 2; MapperEngine::MapperEngine(ptr<GlobalSettings> settings, FileSystem&& resources, ptr<IAppWindow> window) : ClientEngine(settings, std::move(resources), window, [&] { RegisterMapperMetadata(this, &resources); }), ParticleEditors {this} { FO_STACK_TRACE_ENTRY(); GetApp()->LoadImGuiEffect(Resources); MapsFileSys.AddDirSource("", false, true, true); for (const auto& res_pack : Settings->GetResourcePacks()) { for (const auto& dir : res_pack.InputDirs) { MapsFileSys.AddDirSource(dir, true, true, true); } } EffectMngr.LoadDefaultEffects(); SprMngr.RegisterSpriteFactory(SafeAlloc::MakeUnique<DefaultSpriteFactory>(&SprMngr)); SprMngr.RegisterSpriteFactory(SafeAlloc::MakeUnique<ParticleSpriteFactory>(&SprMngr, Settings, &EffectMngr, &GameTime, &Hashes)); #if FO_ENABLE_3D SprMngr.RegisterSpriteFactory(SafeAlloc::MakeUnique<ModelSpriteFactory>(&SprMngr, Settings, this, &EffectMngr, &GameTime, this)); #endif ResMngr.IndexFiles(); MapScriptTypes(this); MapEngineType<PlayerView>(EngineMetadata::GetBaseType(PlayerView::ENTITY_TYPE_NAME)); MapEngineType<ItemView>(EngineMetadata::GetBaseType(ItemView::ENTITY_TYPE_NAME)); MapEngineType<ItemHexView>(EngineMetadata::GetBaseType(ItemView::ENTITY_TYPE_NAME)); MapEngineType<CritterView>(EngineMetadata::GetBaseType(CritterView::ENTITY_TYPE_NAME)); MapEngineType<CritterHexView>(EngineMetadata::GetBaseType(CritterView::ENTITY_TYPE_NAME)); MapEngineType<MapView>(EngineMetadata::GetBaseType(MapView::ENTITY_TYPE_NAME)); MapEngineType<LocationView>(EngineMetadata::GetBaseType(LocationView::ENTITY_TYPE_NAME)); #if FO_ANGELSCRIPT_SCRIPTING InitAngelScriptScripting(this, *Settings, Resources); #endif _curLang = TextPack {&Hashes}; _curLang.LoadFromResources(Resources, Settings->Language); AnimViewer = SafeAlloc::MakeUnique<AnimationViewer>(this, &SprMngr, &ResMngr, &GameTime); PartViewer = SafeAlloc::MakeUnique<ParticleViewer>(this, &SprMngr); SprMngr.BeginScene(); SprMngr.EndScene(); InitIface(); // Initialize tabs const auto& cr_protos = GetProtoCritters(); for (const auto& proto : cr_protos | std::views::values) { Tabs[INT_MODE_CRIT][DEFAULT_SUB_TAB].CritterProtos.emplace_back(proto); Tabs[INT_MODE_CRIT][proto->CollectionName].CritterProtos.emplace_back(proto); } for (auto& proto : Tabs[INT_MODE_CRIT] | std::views::values) { std::ranges::stable_sort(proto.CritterProtos, [](auto&& a, auto&& b) -> bool { return a->GetName() < b->GetName(); }); } const auto& item_protos = GetProtoItems(); for (const auto& proto : item_protos | std::views::values) { Tabs[INT_MODE_ITEM][DEFAULT_SUB_TAB].ItemProtos.emplace_back(proto); Tabs[INT_MODE_ITEM][proto->CollectionName].ItemProtos.emplace_back(proto); } for (auto& proto : Tabs[INT_MODE_ITEM] | std::views::values) { std::ranges::stable_sort(proto.ItemProtos, [](auto&& a, auto&& b) -> bool { return a->GetName() < b->GetName(); }); } for (int32_t i = 0; i < TAB_COUNT; i++) { if (Tabs[i].empty()) { Tabs[i][DEFAULT_SUB_TAB].Scroll = 0; } ActiveSubTabs[i] = &Tabs[i].begin()->second; } // Initialize tabs scroll and names for (int32_t i = INT_MODE_CUSTOM0; i <= INT_MODE_CUSTOM9; i++) { PanelModeNames[i] = "-"; } PanelModeNames[INT_MODE_ITEM] = "Item"; PanelModeNames[INT_MODE_TILE] = "Tile"; PanelModeNames[INT_MODE_CRIT] = "Crit"; PanelModeNames[INT_MODE_FAST] = "Fast"; PanelModeNames[INT_MODE_IGNORE] = "Ign"; PanelModeNames[INT_MODE_INCONT] = "Inv"; PanelModeNames[INT_MODE_MESS] = "Msg"; PanelModeNames[INT_MODE_LIST] = "Maps"; // Start script InitModules(); OnStart.Fire(); if (!Settings->StartMap.empty()) { auto map = LoadMap(Settings->StartMap); if (map) { if (Settings->StartHexX > 0 && Settings->StartHexY > 0) { map->InstantScrollTo(map->GetSize().from_raw_pos(Settings->StartHexX, Settings->StartHexY)); } ShowMap(map); } } ParticleEditors.Initialize(); // Refresh resources after start script executed RefreshActiveProtoLists(); // The cached ImGui window layout only matters for the interactive editor UI. Skip it under the null // renderer (headless mapper, e.g. unit tests and batch map rendering): nothing draws those windows, and // feeding a stale/foreign cached ini into the headless ImGui context is a needless crash surface. if (!Settings->NullRenderer) { string imgui_ini = _uiSettings.GetString(MAPPER_IMGUI_SETTINGS_KEY); if (!imgui_ini.empty()) { ImGui::LoadIniSettingsFromMemory(imgui_ini.c_str(), imgui_ini.size()); ImGui::GetIO().WantSaveIniSettings = false; } } // Load console history string history_str = Cache.GetString("mapper_console.txt"); ConsoleHistory = strex(history_str).normalize_line_endings().split('\n'); while (numeric_cast<int32_t>(ConsoleHistory.size()) > Settings->ConsoleHistorySize) { ConsoleHistory.erase(ConsoleHistory.begin()); } ConsoleHistoryCur = numeric_cast<int32_t>(ConsoleHistory.size()); MapperWindowFocused = SprMngr.IsWindowFocused(); } void MapperEngine::Shutdown() { FO_STACK_TRACE_ENTRY(); AnimViewer->SaveSettings(); PartViewer->SaveSettings(); ParticleEditors.Shutdown(); while (!LoadedMaps.empty()) { UnloadMap(LoadedMaps.back().get(), false); } ClientEngine::Shutdown(); } void MapperEngine::InitIface() { FO_STACK_TRACE_ENTRY(); WriteLog("Init interface"); // Interface MainPanelPos = {-1, -1}; MainPanelWindowRect = MakeRectFromEdges(0, 0, 0, 28); if (MainPanelPos.x == -1) { MainPanelPos.x = 0; } if (MainPanelPos.y == -1) { MainPanelPos.y = 0; } MainPanelContentRect = MakeRectFromEdges(12, 36, 520, 120); WorkspaceWindowVisible = false; ContentWindowVisible = false; MapListWindowVisible = true; MapWindowVisible = true; HistoryWindowVisible = false; SettingsWindowVisible = false; ActivePanelMode = INT_MODE_ITEM; ProtoWidth = 50; ProtosOnScreen = MainPanelContentRect.width / ProtoWidth; MemFill(TabIndex, 0, sizeof(TabIndex)); CritterDir = hdir::SouthWest; CurMode = CUR_MODE_DEFAULT; SelectItemsEnabled = true; SelectSceneryEnabled = true; SelectWallsEnabled = true; SelectCrittersEnabled = true; InspectorVisible = false; // Cursor CurPDef = SprMngr.LoadSprite("CurDefault.png", AtlasType::IfaceSprites); CurPHand = SprMngr.LoadSprite("CurHand.png", AtlasType::IfaceSprites); WriteLog("Init interface complete"); } void MapperEngine::ResetImGuiSettings() { FO_STACK_TRACE_ENTRY(); _uiSettings.Remove(MAPPER_IMGUI_SETTINGS_KEY); ImGui::LoadIniSettingsFromMemory("", 0); ImGui::GetIO().WantSaveIniSettings = false; WorkspaceWindowVisible = false; ContentWindowVisible = false; CritterAnimationsWindowVisible = false; ScriptCallWindowVisible = false; MapListWindowVisible = true; MapWindowVisible = true; HistoryWindowVisible = false; SettingsWindowVisible = false; InspectorVisible = false; InspectorPos = {24, 24}; ParticleEditors.ResetLayout(); AddMess("ImGui layout reset"); } auto MapperEngine::GetPreviewSprite(hstring fname) -> nptr<Sprite> { FO_STACK_TRACE_ENTRY(); if (auto it = PreviewSprites.find(fname); it != PreviewSprites.end()) { return it->second; } auto spr = SprMngr.LoadSprite(fname, AtlasType::IfaceSprites); if (spr) { spr->PlayDefault(); } return PreviewSprites.emplace(fname, std::move(spr)).first->second; } void MapperEngine::SetInputLocked(bool locked) noexcept { FO_STACK_TRACE_ENTRY(); InputLocked = locked; } void MapperEngine::MapperMainLoop() { FO_STACK_TRACE_ENTRY(); FrameAdvance(); OnLoop.Fire(); BeginMapperFrameInput(); if (_curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->Process(); ProcessRightMouseInertia(); } DrawMapperFrame(); if (ResetImGuiSettingsRequested) { ResetImGuiSettingsRequested = false; ResetImGuiSettings(); } auto& io = ImGui::GetIO(); if (io.WantSaveIniSettings) { size_t ini_size = 0; if (auto ini_data = make_nptr(ImGui::SaveIniSettingsToMemory(&ini_size)); ini_data) { _uiSettings.SetString(MAPPER_IMGUI_SETTINGS_KEY, string_view(ini_data.get(), ini_size)); } io.WantSaveIniSettings = false; } } auto MapperEngine::BeginMapperFrameInput() -> bool { FO_STACK_TRACE_ENTRY(); bool window_focused = SprMngr.IsWindowFocused(); if (window_focused && !MapperWindowFocused) { ParticleEditors.OnFocusGained(); } MapperWindowFocused = window_focused; if (InputLocked) { Settings->ScrollMouseRight = false; Settings->ScrollMouseLeft = false; Settings->ScrollMouseDown = false; Settings->ScrollMouseUp = false; Settings->ScrollKeybRight = false; Settings->ScrollKeybLeft = false; Settings->ScrollKeybDown = false; Settings->ScrollKeybUp = false; MouseHoldMode = INT_NONE; RightMouseDragged = false; RightMouseInertia = {}; RightMouseVelocityAccum = {}; RightMouseVelocityTime = {}; } if (bool is_fullscreen = SprMngr.IsFullscreen(); (is_fullscreen && Settings->FullscreenMouseScroll) || (!is_fullscreen && Settings->WindowedMouseScroll)) { if (!InputLocked) { Settings->ScrollMouseRight = MousePos.x >= Settings->ScreenWidth - 1; Settings->ScrollMouseLeft = MousePos.x <= 0; Settings->ScrollMouseDown = MousePos.y >= Settings->ScreenHeight - 1; Settings->ScrollMouseUp = MousePos.y <= 0; } } if (!window_focused) { OnInputLost.Fire(); if (!PendingSelectionMoveEntries.empty()) { CommitPendingSelectionMove(); } MouseHoldMode = INT_NONE; RightMouseDragged = false; RightMouseInertia = {}; } InputEvent ev; while (GetApp()->Input.PollEvent(ev)) { ProcessInputEvent(ev); if (!InputLocked) { ProcessMapperInputEvent(ev); } } return true; } void MapperEngine::ProcessMapperInputEvent(const InputEvent& ev) { FO_STACK_TRACE_ENTRY(); auto ev_type = ev.Type; if (ev_type == InputEvent::EventType::KeyDownEvent || ev_type == InputEvent::EventType::KeyUpEvent) { HandleMapperKeyboardEvent(ev); } else if (ev_type == InputEvent::EventType::MouseWheelEvent) { if (!IsImGuiMouseCaptured() && ev.MouseWheel.Delta != 0 && _curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); float32_t cur_zoom = cur_map->GetSpritesZoomTarget(); ChangeZoom(ScaleZoomValue(cur_zoom, ev.MouseWheel.Delta > 0 ? 1.2f : 0.8f)); } } else if (ev_type == InputEvent::EventType::MouseDownEvent) { if (IsImGuiMouseCaptured()) { return; } if (ev.MouseDown.Button == MouseButton::Middle) { CurMMouseDown(); ChangeZoom(1.0f); } if (ev.MouseDown.Button == MouseButton::Left) { HandleLeftMouseDown(); } if (ev.MouseDown.Button == MouseButton::Right) { MouseHoldMode = INT_PAN; RightMouseDragged = false; RightMouseInertia = {}; RightMouseVelocityAccum = {}; RightMouseVelocityTime = nanotime::now(); } } else if (ev_type == InputEvent::EventType::MouseUpEvent) { if (IsImGuiMouseCaptured()) { return; } if (ev.MouseUp.Button == MouseButton::Left) { HandleLeftMouseUp(); } if (ev.MouseUp.Button == MouseButton::Right) { CurRMouseUp(); } } else if (ev_type == InputEvent::EventType::MouseMoveEvent) { if (_curMap && MouseHoldMode == INT_PAN) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); float32_t zoom = cur_map->GetSpritesZoom(); fpos32 pan_delta {-numeric_cast<float32_t>(ev.MouseMove.DeltaX) / zoom, -numeric_cast<float32_t>(ev.MouseMove.DeltaY) / zoom}; fpos32 screen_pan_delta {-numeric_cast<float32_t>(ev.MouseMove.DeltaX), -numeric_cast<float32_t>(ev.MouseMove.DeltaY)}; if (ev.MouseMove.DeltaX != 0 || ev.MouseMove.DeltaY != 0) { RightMouseDragged = true; cur_map->InstantScroll(pan_delta); RightMouseVelocityAccum += screen_pan_delta; nanotime now_time = nanotime::now(); float32_t sample_ms = (now_time - RightMouseVelocityTime).to_ms<float32_t>(); if (sample_ms >= 8.0f) { RightMouseInertia = RightMouseVelocityAccum * (1000.0f / sample_ms); RightMouseVelocityAccum = {}; RightMouseVelocityTime = now_time; } } } else { HandleSelectionMouseDrag(); } } } void MapperEngine::DrawMapperFrame() { FO_STACK_TRACE_ENTRY(); EffectMngr.UpdateEffects(GameTime); FontMngr.FrameUpdate(); { SprMngr.BeginScene(); if (_curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->DrawMap(); } SpritesCanDraw = true; OnRenderIface.Fire(); SpritesCanDraw = false; DrawMainPanelImGui(); DrawConsoleImGui(); DrawInspectorImGui(); CurDraw(); SprMngr.EndScene(); } } void MapperEngine::ProcessRightMouseInertia() { FO_STACK_TRACE_ENTRY(); if (InputLocked || !_curMap || MouseHoldMode == INT_PAN) { return; } if (RightMouseInertia.dist() < 30.0f) { RightMouseInertia = {}; return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); float32_t dt_ms = std::max(GameTime.GetFrameDeltaTime().to_ms<float32_t>(), 1.0f); float32_t dt_sec = dt_ms / 1000.0f; float32_t zoom = std::max(cur_map->GetSpritesZoom(), 0.001f); cur_map->InstantScroll((RightMouseInertia * dt_sec) / zoom); float32_t damping = std::pow(0.9f, dt_ms / 16.6667f); RightMouseInertia *= damping; } void MapperEngine::ResetPendingSelectionMove() { FO_STACK_TRACE_ENTRY(); PendingSelectionMoveEntries.clear(); } void MapperEngine::CommitPendingSelectionMove() { FO_STACK_TRACE_ENTRY(); if (PendingSelectionMoveEntries.empty() || !_curMap) { ResetPendingSelectionMove(); return; } auto move_entries = std::move(PendingSelectionMoveEntries); ResetPendingSelectionMove(); PushUndoOp(GetCurMap(), UndoOp {"Move selection", [move_entries](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { for (const auto& entry : move_entries) { auto target = mapper->FindEntityById(*active_map, entry.EntityId); if (!target) { return false; } if (auto item = target.dyn_cast<ItemHexView>()) { if (entry.HasOffset) { item->SetOffset(entry.OldOffset); item->RefreshAnim(); } if (entry.HasHex) { if (!entry.OldMultihexMesh.empty()) { item->SetMultihexMesh(entry.OldMultihexMesh); } (*active_map)->MoveItem(item, entry.OldHex); } } else if (auto cr = target.dyn_cast<CritterHexView>(); cr && entry.HasHex) { (*active_map)->MoveCritter(cr, entry.OldHex, false); } } mapper->SetMapDirty(*active_map); (*active_map)->RebuildMap(); return true; }, [move_entries](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { for (const auto& entry : move_entries) { auto target = mapper->FindEntityById(*active_map, entry.EntityId); if (!target) { return false; } if (auto item = target.dyn_cast<ItemHexView>()) { if (entry.HasOffset) { item->SetOffset(entry.NewOffset); item->RefreshAnim(); } if (entry.HasHex) { if (!entry.NewMultihexMesh.empty()) { item->SetMultihexMesh(entry.NewMultihexMesh); } (*active_map)->MoveItem(item, entry.NewHex); } } else if (auto cr = target.dyn_cast<CritterHexView>(); cr && entry.HasHex) { (*active_map)->MoveCritter(cr, entry.NewHex, false); } } mapper->SetMapDirty(*active_map); (*active_map)->RebuildMap(); return true; }}); } void MapperEngine::HandleMapperKeyboardEvent(const InputEvent& ev) { FO_STACK_TRACE_ENTRY(); auto ev_type = ev.Type; auto dikdw = ev_type == InputEvent::EventType::KeyDownEvent ? ev.KeyDown.Code : KeyCode::None; auto dikup = ev_type == InputEvent::EventType::KeyUpEvent ? ev.KeyUp.Code : KeyCode::None; // Avoid repeating if (dikdw != KeyCode::None && PressedKeys[static_cast<int32_t>(dikdw)]) { return; } if (dikup != KeyCode::None && !PressedKeys[static_cast<int32_t>(dikup)]) { return; } // Keyboard states, to know outside function PressedKeys[static_cast<int32_t>(dikup)] = false; PressedKeys[static_cast<int32_t>(dikdw)] = true; bool block_hotkeys = IsImGuiTextInputActive(); HandlePrimaryMapperHotkeys(dikdw, block_hotkeys); if (!block_hotkeys && !GetApp()->Input.IsAltDown() && !GetApp()->Input.IsCtrlDown() && !GetApp()->Input.IsShiftDown() && (dikdw == KeyCode::F11 || dikdw == KeyCode::F12)) { return; } HandleShiftMapperHotkeys(dikdw, block_hotkeys); HandleCtrlMapperHotkeys(dikdw, block_hotkeys); if (dikdw != KeyCode::None) { HandleMapperConsoleKeyDown(dikdw, ev.KeyDown.Text); } UpdateArrowScrollKeys(dikdw, dikup); } void MapperEngine::HandlePrimaryMapperHotkeys(KeyCode dikdw, bool block_hotkeys) { FO_STACK_TRACE_ENTRY(); if (block_hotkeys || GetApp()->Input.IsAltDown() || GetApp()->Input.IsCtrlDown() || GetApp()->Input.IsShiftDown()) { return; } switch (dikdw) { case KeyCode::F1: ToggleMapVisibilityFlag(GetCurMap(), Settings->ShowItem); break; case KeyCode::F2: ToggleMapVisibilityFlag(GetCurMap(), Settings->ShowScen); break; case KeyCode::F3: ToggleMapVisibilityFlag(GetCurMap(), Settings->ShowWall); break; case KeyCode::F4: ToggleMapVisibilityFlag(GetCurMap(), Settings->ShowCrit); break; case KeyCode::F5: ToggleMapVisibilityFlag(GetCurMap(), Settings->ShowTile); break; case KeyCode::F6: ToggleMapVisibilityFlag(GetCurMap(), Settings->ShowFast); break; case KeyCode::F7: WorkspaceWindowVisible = !WorkspaceWindowVisible; break; case KeyCode::F8: if (SprMngr.IsFullscreen()) { Settings->FullscreenMouseScroll = !Settings->FullscreenMouseScroll; } else { Settings->WindowedMouseScroll = !Settings->WindowedMouseScroll; } break; case KeyCode::F9: if (InspectorVisible) { SelectClear(); } else if (!SelectedEntities.empty() || InContItem) { InspectorVisible = true; } break; case KeyCode::F10: ToggleMapperHexOverlay(); break; case KeyCode::F11: SprMngr.ToggleFullscreen(); break; case KeyCode::F12: SprMngr.MinimizeWindow(); break; case KeyCode::Delete: SelectDelete(); break; case KeyCode::Escape: if (CancelInspectorPropertyEdit()) { break; } if (!SelectedEntities.empty() || InContItem) { SelectClear(); } else if (CurMode == CUR_MODE_PLACE_OBJECT) { MouseHoldMode = INT_NONE; SetCurMode(CUR_MODE_DEFAULT); } break; case KeyCode::Add: if (_curMap && !ConsoleEdit && SelectedEntities.empty()) { SetGlobalDayTime(ShiftDayTimeWithWrap(GetGlobalDayTime(), 60)); } break; case KeyCode::Subtract: if (_curMap && !ConsoleEdit && SelectedEntities.empty()) { SetGlobalDayTime(ShiftDayTimeWithWrap(GetGlobalDayTime(), -60)); } break; case KeyCode::Tab: SelectAxialGrid = !SelectAxialGrid; break; default: break; } } void MapperEngine::HandleShiftMapperHotkeys(KeyCode dikdw, bool block_hotkeys) { FO_STACK_TRACE_ENTRY(); if (block_hotkeys || !GetApp()->Input.IsShiftDown()) { return; } switch (dikdw) { case KeyCode::F7: ContentWindowVisible = !ContentWindowVisible; break; case KeyCode::F11: SprMngr.GetAtlasMngr()->DumpAtlases(); break; case KeyCode::C0: case KeyCode::Numpad0: case KeyCode::C1: case KeyCode::Numpad1: case KeyCode::C2: case KeyCode::Numpad2: case KeyCode::C3: case KeyCode::Numpad3: case KeyCode::C4: case KeyCode::Numpad4: TileLayer = GetTileLayerFromKey(dikdw).value_or(TileLayer); break; case KeyCode::Tab: SelectEntireEntity = !SelectEntireEntity; break; default: break; } } void MapperEngine::HandleCtrlMapperHotkeys(KeyCode dikdw, bool block_hotkeys) { FO_STACK_TRACE_ENTRY(); if (block_hotkeys || !GetApp()->Input.IsCtrlDown()) { return; } switch (dikdw) { case KeyCode::Z: ExecuteUndo(); break; case KeyCode::Y: ExecuteRedo(); break; case KeyCode::X: BufferCut(); break; case KeyCode::C: BufferCopy(); break; case KeyCode::V: BufferPaste(); break; case KeyCode::A: SelectAll(); break; case KeyCode::S: if (_curMap) { SaveCurrentMap(); } break; case KeyCode::D: if (_curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->SetScrollCheck(!cur_map->IsScrollCheck()); } break; case KeyCode::B: MarkBlockedHexes(); break; default: break; } } void MapperEngine::UpdateArrowScrollKeys(KeyCode dikdw, KeyCode dikup) { FO_STACK_TRACE_ENTRY(); if (dikdw != KeyCode::None && !ConsoleEdit) { switch (dikdw) { case KeyCode::Left: Settings->ScrollKeybLeft = true; break; case KeyCode::Right: Settings->ScrollKeybRight = true; break; case KeyCode::Up: Settings->ScrollKeybUp = true; break; case KeyCode::Down: Settings->ScrollKeybDown = true; break; default: break; } } if (dikup != KeyCode::None) { switch (dikup) { case KeyCode::Left: Settings->ScrollKeybLeft = false; break; case KeyCode::Right: Settings->ScrollKeybRight = false; break; case KeyCode::Up: Settings->ScrollKeybUp = false; break; case KeyCode::Down: Settings->ScrollKeybDown = false; break; default: break; } } } void MapperEngine::HandleMapperConsoleKeyDown(KeyCode dikdw, string_view key_text) { FO_STACK_TRACE_ENTRY(); ignore_unused(key_text); if (!ConsoleEdit && dikdw == KeyCode::Grave) { ConsoleEdit = true; ConsoleStr.clear(); ConsoleHistoryCur = numeric_cast<int32_t>(ConsoleHistory.size()); } } void MapperEngine::ChangeZoom(float32_t new_zoom) { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); fpos32 mouse_pos = fpos32(GetApp()->Input.GetMousePosition()); fsize32 screen_size = fsize32(cur_map->GetScreenSize()); float32_t mouse_x_factor = std::clamp(mouse_pos.x / screen_size.width, 0.0f, 1.0f); float32_t mouse_y_factor = std::clamp(mouse_pos.y / screen_size.height, 0.0f, 1.0f); cur_map->ChangeZoom(new_zoom, {mouse_x_factor, mouse_y_factor}); } auto MapperEngine::IsImGuiMouseCaptured() const -> bool { FO_STACK_TRACE_ENTRY(); return ImGui::GetIO().WantCaptureMouse; } auto MapperEngine::IsImGuiTextInputActive() const -> bool { FO_STACK_TRACE_ENTRY(); return ImGui::GetIO().WantTextInput; } MapperEngine::EntityBuf::EntityBuf(const EntityBuf& other) : Id(other.Id), Hex(other.Hex), Dir(other.Dir), IsCritter(other.IsCritter), IsItem(other.IsItem), Slot(other.Slot), StackId(other.StackId), Proto(other.Proto) { FO_STACK_TRACE_ENTRY(); if (other.Props) { Props.emplace(other.Props->Copy()); } Children.reserve(other.Children.size()); for (const auto& child : other.Children) { Children.emplace_back(SafeAlloc::MakeUnique<EntityBuf>(*child)); } } auto MapperEngine::EntityBuf::operator=(const EntityBuf& other) -> EntityBuf& { FO_STACK_TRACE_ENTRY(); if (this != &other) { Id = other.Id; Hex = other.Hex; Dir = other.Dir; IsCritter = other.IsCritter; IsItem = other.IsItem; Slot = other.Slot; StackId = other.StackId; Proto = other.Proto; if (other.Props) { Props.emplace(other.Props->Copy()); } else { Props.reset(); } Children.clear(); Children.reserve(other.Children.size()); for (const auto& child : other.Children) { Children.emplace_back(SafeAlloc::MakeUnique<EntityBuf>(*child)); } } return *this; } MapperEngine::UndoOp::UndoOp(string label, std::function<bool(ptr<MapperEngine>, ptr<ptr<MapView>>)> undo, std::function<bool(ptr<MapperEngine>, ptr<ptr<MapView>>)> redo, bool is_snapshot) : Label(std::move(label)), IsSnapshot(is_snapshot), Undo(std::move(undo)), Redo(std::move(redo)) { FO_STACK_TRACE_ENTRY(); } auto MapperEngine::GetUndoContext(nptr<MapView> map, bool create) -> nptr<UndoContext> { FO_STACK_TRACE_ENTRY(); if (!map) { return nullptr; } if (!create) { if (auto it = UndoContexts.find(map); it != UndoContexts.end()) { return &it->second; } return nullptr; } return &UndoContexts[map]; } auto MapperEngine::GetUndoContext(nptr<const MapView> map, bool create) const -> nptr<const UndoContext> { FO_STACK_TRACE_ENTRY(); if (!map) { return nullptr; } auto map_ptr = make_ptr(const_cast<MapView*>(std::addressof(*map))); if (!create) { if (auto it = UndoContexts.find(map_ptr); it != UndoContexts.end()) { return &it->second; } } return nullptr; } void MapperEngine::ClearUndoContext(nptr<MapView> map) { FO_STACK_TRACE_ENTRY(); if (!map) { return; } UndoContexts.erase(map); } void MapperEngine::RemapUndoContext(nptr<MapView> old_map, nptr<MapView> new_map) { FO_STACK_TRACE_ENTRY(); if (!old_map || !new_map || old_map == new_map) { return; } if (auto it = UndoContexts.find(old_map); it != UndoContexts.end()) { auto ctx = std::move(it->second); UndoContexts.erase(it); UndoContexts[new_map] = std::move(ctx); } } void MapperEngine::PushUndoOp(nptr<MapView> map, UndoOp op) { FO_STACK_TRACE_ENTRY(); if (UndoRedoInProgress || !map || !op.Undo || !op.Redo) { return; } auto ctx = GetUndoContext(map, true); FO_VERIFY_AND_THROW(ctx, "Missing script execution context"); ctx->RedoStack.clear(); ctx->UndoStack.emplace_back(std::move(op)); if (ctx->UndoStack.size() > MaxUndoDepth) { ctx->UndoStack.erase(ctx->UndoStack.begin()); if (ctx->CleanUndoDepth > 0) { ctx->CleanUndoDepth--; } else if (ctx->CleanUndoDepth == 0) { ctx->CleanUndoDepth = -1; } } } auto MapperEngine::CanUndo() const -> bool { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return false; } if (auto ctx = GetUndoContext(GetCurMap(), false)) { return !ctx->UndoStack.empty(); } return false; } auto MapperEngine::CanRedo() const -> bool { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return false; } if (auto ctx = GetUndoContext(GetCurMap(), false)) { return !ctx->RedoStack.empty(); } return false; } auto MapperEngine::GetUndoLabel() const -> string { FO_STACK_TRACE_ENTRY(); if (auto ctx = GetUndoContext(GetCurMap(), false)) { if (!ctx->UndoStack.empty()) { return ctx->UndoStack.back().Label; } } return {}; } auto MapperEngine::GetRedoLabel() const -> string { FO_STACK_TRACE_ENTRY(); if (auto ctx = GetUndoContext(GetCurMap(), false)) { if (!ctx->RedoStack.empty()) { return ctx->RedoStack.back().Label; } } return {}; } auto MapperEngine::ExecuteUndo() -> bool { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return false; } auto map = GetCurMap(); FO_VERIFY_AND_THROW(map, "Map is null"); auto ctx = GetUndoContext(map, false); if (!ctx) { return false; } if (ctx->UndoStack.empty()) { return false; } auto op = std::move(ctx->UndoStack.back()); ctx->UndoStack.pop_back(); ptr<MapView> active_map = map; UndoRedoInProgress = true; auto progress_guard = scope_fail([this]() noexcept { UndoRedoInProgress = false; }); bool ok = op.Undo(this, &active_map); UndoRedoInProgress = false; if (!ok) { auto rollback_ctx = GetUndoContext(map, true); FO_VERIFY_AND_THROW(rollback_ctx, "Rollback context is null"); rollback_ctx->UndoStack.emplace_back(std::move(op)); return false; } auto active_ctx = GetUndoContext(active_map, true); FO_VERIFY_AND_THROW(active_ctx, "Active context is null"); active_ctx->RedoStack.emplace_back(std::move(op)); SetMapDirty(active_map, active_ctx->CleanUndoDepth < 0 || numeric_cast<int32_t>(active_ctx->UndoStack.size()) != active_ctx->CleanUndoDepth); return true; } auto MapperEngine::ExecuteRedo() -> bool { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return false; } auto map = GetCurMap(); FO_VERIFY_AND_THROW(map, "Map is null"); auto ctx = GetUndoContext(map, false); if (!ctx) { return false; } if (ctx->RedoStack.empty()) { return false; } auto op = std::move(ctx->RedoStack.back()); ctx->RedoStack.pop_back(); ptr<MapView> active_map = map; UndoRedoInProgress = true; auto progress_guard = scope_fail([this]() noexcept { UndoRedoInProgress = false; }); bool ok = op.Redo(this, &active_map); UndoRedoInProgress = false; if (!ok) { auto rollback_ctx = GetUndoContext(map, true); FO_VERIFY_AND_THROW(rollback_ctx, "Rollback context is null"); rollback_ctx->RedoStack.emplace_back(std::move(op)); return false; } auto active_ctx = GetUndoContext(active_map, true); FO_VERIFY_AND_THROW(active_ctx, "Active context is null"); active_ctx->UndoStack.emplace_back(std::move(op)); SetMapDirty(active_map, active_ctx->CleanUndoDepth < 0 || numeric_cast<int32_t>(active_ctx->UndoStack.size()) != active_ctx->CleanUndoDepth); return true; } auto MapperEngine::CaptureMapSnapshot(nptr<const MapView> map) const -> string { FO_STACK_TRACE_ENTRY(); if (!map) { return {}; } return map->SaveToText(map->GetName()); } void MapperEngine::CaptureEntityBuf(EntityBuf& entity_buf, ptr<ClientEntity> entity) const { FO_STACK_TRACE_ENTRY(); auto cr = entity.dyn_cast<CritterHexView>(); auto item_view = entity.dyn_cast<ItemView>(); auto entity_with_proto = entity.dyn_cast<EntityWithProto>(); FO_VERIFY_AND_THROW(entity_with_proto, "Captured entity does not have an associated prototype"); entity_buf.Id = entity->GetId(); entity_buf.IsCritter = !!cr; entity_buf.IsItem = !!item_view; entity_buf.Proto = entity_with_proto->GetProto(); entity_buf.Props.emplace(entity->GetProperties()->Copy()); if (cr) { entity_buf.Hex = cr->GetHex(); entity_buf.Dir = cr->GetDir(); } else if (auto item = entity.dyn_cast<ItemHexView>()) { entity_buf.Hex = item->GetHex(); entity_buf.Slot = item->GetCritterSlot(); entity_buf.StackId = any_t {string(item->GetContainerStack())}; } else if (item_view) { entity_buf.Slot = item_view->GetCritterSlot(); entity_buf.StackId = any_t {string(item_view->GetContainerStack())}; } vector<refcount_ptr<ItemView>> children = GetEntityInnerItems(entity); for (size_t i = 0; i < children.size(); i++) { auto child_buf = SafeAlloc::MakeUnique<EntityBuf>(); CaptureEntityBuf(*child_buf, children[i]); entity_buf.Children.emplace_back(std::move(child_buf)); } } void MapperEngine::RestoreEntityBufChildren(const EntityBuf& entity_buf, ptr<ItemView> item) { FO_STACK_TRACE_ENTRY(); for (const auto& child_buf : entity_buf.Children) { auto inner_item = item->AddMapperInnerItem(child_buf->Id, child_buf->Proto.dyn_cast<const ProtoItem>(), child_buf->StackId, child_buf->GetProps()); RestoreEntityBufChildren(*child_buf, inner_item); } } auto MapperEngine::RestoreEntityBuf(const EntityBuf& entity_buf, nptr<Entity> owner) -> nptr<ClientEntity> { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(_curMap, "Mapper has no current map"); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!owner) { if (entity_buf.IsCritter) { auto cr = cur_map->AddMapperCritter(entity_buf.Proto->GetProtoId(), entity_buf.Hex, entity_buf.Dir, entity_buf.GetProps(), entity_buf.Id); for (const auto& child_buf : entity_buf.Children) { auto inv_item = cr->AddMapperInvItem(child_buf->Id, child_buf->Proto.dyn_cast<const ProtoItem>(), child_buf->Slot, child_buf->GetProps()); RestoreEntityBufChildren(*child_buf, inv_item); } return cr; } if (entity_buf.IsItem) { auto item = cur_map->AddMapperItem(entity_buf.Proto->GetProtoId(), entity_buf.Hex, entity_buf.GetProps(), entity_buf.Id); RestoreEntityBufChildren(entity_buf, item); return item; } return nullptr; } if (auto cr = owner.dyn_cast<CritterView>()) { auto item = cr->AddMapperInvItem(entity_buf.Id, entity_buf.Proto.dyn_cast<const ProtoItem>(), entity_buf.Slot, entity_buf.GetProps()); RestoreEntityBufChildren(entity_buf, item); return item; } if (auto item_owner = owner.dyn_cast<ItemView>()) { auto item = item_owner->AddMapperInnerItem(entity_buf.Id, entity_buf.Proto.dyn_cast<const ProtoItem>(), entity_buf.StackId, entity_buf.GetProps()); RestoreEntityBufChildren(entity_buf, item); return item; } return nullptr; } auto MapperEngine::FindEntityById(ptr<MapView> map, ident_t id) -> nptr<ClientEntity> { FO_STACK_TRACE_ENTRY(); if (!id) { return nullptr; } if (auto cr = map->GetCritter(id)) { return cr; } if (auto item = map->GetItem(id)) { return item; } std::function<nptr<ClientEntity>(ptr<ItemView>)> find_inner_item = [&](ptr<ItemView> owner) -> nptr<ClientEntity> { span<refcount_ptr<ItemView>> inner_items = owner->GetInnerItems(); for (size_t i = 0; i < inner_items.size(); i++) { auto inner_item = inner_items[i].as_ptr(); if (inner_item->GetId() == id) { return inner_item; } if (auto found = find_inner_item(inner_item)) { return found; } } return nullptr; }; span<refcount_ptr<CritterHexView>> critters = map->GetCritters(); for (size_t cr_index = 0; cr_index < critters.size(); cr_index++) { span<refcount_ptr<ItemView>> inv_items = critters[cr_index]->GetInvItems(); for (size_t item_index = 0; item_index < inv_items.size(); item_index++) { auto inv_item = inv_items[item_index].as_ptr(); if (inv_item->GetId() == id) { return inv_item; } if (auto found = find_inner_item(inv_item)) { return found; } } } span<refcount_ptr<ItemHexView>> map_items = map->GetItems(); for (size_t i = 0; i < map_items.size(); i++) { if (auto found = find_inner_item(map_items[i])) { return found; } } return nullptr; } auto MapperEngine::RestoreMapSnapshot(ptr<ptr<MapView>> map, string_view map_name, const string& map_text) -> bool { FO_STACK_TRACE_ENTRY(); auto old_map = *map; auto restored_map = LoadMapFromText(map_name, map_name, map_text); if (!restored_map) { return false; } UnloadMap(old_map, false); RemapUndoContext(old_map, restored_map); ShowMap(restored_map); *map = restored_map; return true; } auto MapperEngine::ApplyEntityPropertyText(ptr<Entity> entity, ptr<const Property> prop, string_view value_text) -> bool { FO_STACK_TRACE_ENTRY(); try { entity->GetPropertiesForEdit()->ApplyPropertyFromText(prop, value_text); SetMapDirty(GetCurMap()); if (auto item = entity.dyn_cast<ItemHexView>()) { if (item->GetMultihexGeneration() == MultihexGenerationType::SameSibling) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); for (int32_t i = 0; i < GameSettings::MAP_DIR_COUNT; i++) { if (mpos hex = item->GetHex(); GeometryHelper::MoveHexByDir(hex, hdir(i), cur_map->GetSize())) { vector<ptr<ItemHexView>> main_mesh_items = vec_filter(cur_map->GetItemsOnHex(hex), [&](ptr<ItemHexView> item2) -> bool { if (SelectedEntitiesSet.contains(item2)) { return false; } return item->GetProtoId() == item2->GetProtoId(); }); for (ptr<ItemHexView> item2 : main_mesh_items) { item2->GetPropertiesForEdit()->ApplyPropertyFromText(prop, value_text); } } } } } } catch (const std::exception&) { return false; } if (auto hex_item = entity.dyn_cast<ItemHexView>()) { auto offset_prop = hex_item->GetPropertyOffset(); auto pic_map_prop = hex_item->GetPropertyPicMap(); if (prop == offset_prop) { hex_item->RefreshOffs(); } if (prop == pic_map_prop) { hex_item->RefreshAnim(); } } return true; } void MapperEngine::DrawMainPanelImGui() { FO_STACK_TRACE_ENTRY(); if (ImGui::BeginMainMenuBar()) { auto pos = ImGui::GetWindowPos(); auto size = ImGui::GetWindowSize(); MainPanelPos = {iround<int32_t>(pos.x), iround<int32_t>(pos.y)}; MainPanelWindowRect = {0, 0, iround<int32_t>(size.x), iround<int32_t>(size.y)}; MainPanelContentRect = {12, MainPanelWindowRect.height + 8, 520, 120}; ProtosOnScreen = std::max(1, ProtoWidth > 0 ? MainPanelContentRect.width / ProtoWidth : 1); auto run_menu_action = [](bool triggered, auto&& action) { if (triggered) { action(); } }; auto run_menu_action_with_message = [&](bool triggered, auto&& action, string_view message) { run_menu_action(triggered, [&] { action(); AddMess(message); }); }; if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Save current", "Ctrl+S", false, static_cast<bool>(_curMap))) { SaveCurrentMap(); } if (ImGui::MenuItem("Reset changes", nullptr, false, _curMap && IsMapDirty(GetCurMap()))) { ResetCurrentMapChanges(); } ImGui::Separator(); if (ImGui::MenuItem("Exit")) { GetApp()->RequestQuit(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Windows")) { ImGui::MenuItem("Workspace", "F7", &WorkspaceWindowVisible); ImGui::MenuItem("Content", "Shift+F7", &ContentWindowVisible); if (ImGui::MenuItem("Console", "~", ConsoleEdit)) { ConsoleEdit = !ConsoleEdit; if (ConsoleEdit) { ConsoleHistoryCur = numeric_cast<int32_t>(ConsoleHistory.size()); } } ImGui::MenuItem("Critter animations", nullptr, &CritterAnimationsWindowVisible); bool anim_viewer_visible = AnimViewer->IsVisible(); if (ImGui::MenuItem("Animation viewer", nullptr, &anim_viewer_visible)) { AnimViewer->SetVisible(anim_viewer_visible); } bool particle_viewer_visible = PartViewer->IsVisible(); if (ImGui::MenuItem("Particle viewer", nullptr, &particle_viewer_visible)) { PartViewer->SetVisible(particle_viewer_visible); } ImGui::MenuItem("Script call", nullptr, &ScriptCallWindowVisible); ImGui::MenuItem("Map browser", nullptr, &MapListWindowVisible); ImGui::MenuItem("Controls", nullptr, &MapWindowVisible, static_cast<bool>(_curMap)); ImGui::MenuItem("History", nullptr, &HistoryWindowVisible, static_cast<bool>(_curMap)); ParticleEditors.DrawMenuItems(); ImGui::MenuItem("Settings", nullptr, &SettingsWindowVisible); ImGui::EndMenu(); } if (ImGui::BeginMenu("Edit")) { string undo_label = GetUndoLabel(); string redo_label = GetRedoLabel(); if (ImGui::MenuItem(undo_label.empty() ? "Undo" : strex("Undo {}", undo_label).c_str(), "Ctrl+Z", false, CanUndo())) { ExecuteUndo(); } if (ImGui::MenuItem(redo_label.empty() ? "Redo" : strex("Redo {}", redo_label).c_str(), "Ctrl+Y", false, CanRedo())) { ExecuteRedo(); } ImGui::Separator(); if (ImGui::MenuItem("Select all", "Ctrl+A")) { SelectAll(); } if (ImGui::MenuItem("Clear selection")) { SelectClear(); } if (ImGui::MenuItem("Delete selected", "Del")) { SelectDelete(); } ImGui::Separator(); run_menu_action_with_message(ImGui::MenuItem("Copy", "Ctrl+C"), [&] { BufferCopy(); }, "Copied selection"); run_menu_action_with_message(ImGui::MenuItem("Cut", "Ctrl+X"), [&] { BufferCut(); }, "Cut selection"); run_menu_action_with_message(ImGui::MenuItem("Paste", "Ctrl+V"), [&] { BufferPaste(); }, "Pasted selection"); ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { bool view_layer_changed = false; view_layer_changed |= ImGui::MenuItem("Items", nullptr, &Settings->ShowItem); view_layer_changed |= ImGui::MenuItem("Scenery", nullptr, &Settings->ShowScen); view_layer_changed |= ImGui::MenuItem("Walls", nullptr, &Settings->ShowWall); view_layer_changed |= ImGui::MenuItem("Critters", nullptr, &Settings->ShowCrit); view_layer_changed |= ImGui::MenuItem("Tiles", nullptr, &Settings->ShowTile); view_layer_changed |= ImGui::MenuItem("Roof", nullptr, &Settings->ShowRoof); view_layer_changed |= ImGui::MenuItem("Fast", nullptr, &Settings->ShowFast); if (view_layer_changed && _curMap) { _curMap->RebuildMap(); } ImGui::Separator(); ImGui::MenuItem("Axial grid selection", nullptr, &SelectAxialGrid); ImGui::MenuItem("Select entire entity", nullptr, &SelectEntireEntity); ImGui::EndMenu(); } if (ImGui::BeginMenu("Tools")) { run_menu_action_with_message( ImGui::MenuItem("Rebuild map", nullptr, false, static_cast<bool>(_curMap)), [&] { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->RebuildMap(); }, "Map rebuilt"); run_menu_action_with_message(ImGui::MenuItem("Mark blocked hexes", nullptr, false, static_cast<bool>(_curMap)), [&] { MarkBlockedHexes(); }, "Blocked hexes marked"); run_menu_action_with_message(ImGui::MenuItem("Reverse lights", nullptr, false, static_cast<bool>(_curMap)), [&] { ParseCommand("* reverse-light"); }, "Reverse lights done"); run_menu_action_with_message(ImGui::MenuItem("Merge by command", nullptr, false, static_cast<bool>(_curMap)), [&] { ParseCommand("* merge-items"); }, "Merge items command done"); run_menu_action_with_message(ImGui::MenuItem("Break by command", nullptr, false, static_cast<bool>(_curMap)), [&] { ParseCommand("* break-items"); }, "Break items command done"); ImGui::Separator(); if (ImGui::MenuItem("Merge multihex items", nullptr, false, static_cast<bool>(_curMap))) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); size_t merged = MergeItemsToMultihexMeshes(cur_map); AddMess(strex("Merged items: {}", merged)); } if (ImGui::MenuItem("Break multihex items", nullptr, false, static_cast<bool>(_curMap))) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); size_t broken = BreakItemsMultihexMeshes(cur_map); AddMess(strex("Broken items: {}", broken)); } ImGui::EndMenu(); } if (ImGui::BeginMenu("System")) { if (ImGui::MenuItem("Toggle fullscreen", "F11")) { SprMngr.ToggleFullscreen(); } if (ImGui::MenuItem("Minimize", "F12")) { SprMngr.MinimizeWindow(); } if (ImGui::MenuItem("Dump atlases")) { SprMngr.GetAtlasMngr()->DumpAtlases(); } ImGui::Separator(); if (SprMngr.IsFullscreen()) { ImGui::MenuItem("Fullscreen mouse scroll", nullptr, &Settings->FullscreenMouseScroll); } else { ImGui::MenuItem("Windowed mouse scroll", nullptr, &Settings->WindowedMouseScroll); } ImGui::EndMenu(); } if (_curMap && IsMapDirty(GetCurMap())) { constexpr string_view_nt dirty_label = "*** Save ***"; float32_t label_width = ImGui::CalcTextSize(dirty_label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2.0f; float32_t right_x = numeric_cast<float32_t>(MainPanelWindowRect.width) - label_width - ImGui::GetStyle().ItemSpacing.x * 2.0f; if (ImGui::GetCursorPosX() < right_x) { ImGui::SetCursorPosX(right_x); } ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.72f, 0.45f, 0.10f, 0.95f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.86f, 0.56f, 0.16f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.60f, 0.36f, 0.08f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.08f, 0.08f, 0.08f, 1.0f)); if (ImGui::Button(dirty_label.c_str())) { SaveCurrentMap(); } ImGui::PopStyleColor(4); } ImGui::EndMainMenuBar(); } DrawWorkspaceWindowImGui(); DrawContentWindowImGui(); DrawCritterAnimationsWindowImGui(); AnimViewer->Draw(); PartViewer->Draw(); DrawScriptCallWindowImGui(); DrawMapListWindowImGui(); DrawMapWindowImGui(); DrawHistoryWindowImGui(); ParticleEditors.DrawWindows(); DrawSettingsWindowImGui(); } void MapperEngine::DrawWorkspaceWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!WorkspaceWindowVisible) { return; } ImGui::SetNextWindowPos( { numeric_cast<float32_t>(MainPanelPos.x + MainPanelContentRect.x), numeric_cast<float32_t>(MainPanelPos.y + MainPanelContentRect.y), }, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({500.0f, 600.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("Workspace", &WorkspaceWindowVisible, 0)) { ImGui::End(); return; } auto toggle_visibility = [&](string_view_nt label, string_view_nt tooltip, bool& value) { if (value) { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); } bool clicked = ImGui::Button(label.c_str(), {32.0f, 0.0f}); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("%s", tooltip.c_str()); } if (value) { ImGui::PopStyleColor(2); } if (clicked) { value = !value; } return clicked; }; bool visibility_changed = false; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {4.0f, ImGui::GetStyle().ItemSpacing.y}); visibility_changed |= toggle_visibility("Items", "Items", Settings->ShowItem); ImGui::SameLine(); visibility_changed |= toggle_visibility("Scenery", "Scenery", Settings->ShowScen); ImGui::SameLine(); visibility_changed |= toggle_visibility("Walls", "Walls", Settings->ShowWall); ImGui::SameLine(); visibility_changed |= toggle_visibility("Critters", "Critters", Settings->ShowCrit); ImGui::SameLine(); visibility_changed |= toggle_visibility("Tiles", "Tiles", Settings->ShowTile); ImGui::SameLine(); visibility_changed |= toggle_visibility("Roof", "Roof", Settings->ShowRoof); ImGui::SameLine(); visibility_changed |= toggle_visibility("Fast", "Fast", Settings->ShowFast); ImGui::PopStyleVar(); if (visibility_changed && _curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->RebuildMap(); } ImGui::Separator(); constexpr array tab_order { INT_MODE_CUSTOM0, INT_MODE_CUSTOM1, INT_MODE_CUSTOM2, INT_MODE_CUSTOM3, INT_MODE_CUSTOM4, INT_MODE_CUSTOM5, INT_MODE_CUSTOM6, INT_MODE_CUSTOM7, INT_MODE_CUSTOM8, INT_MODE_CUSTOM9, INT_MODE_ITEM, INT_MODE_TILE, INT_MODE_CRIT, INT_MODE_FAST, INT_MODE_IGNORE, }; constexpr int32_t workspace_table_flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp; if (ImGui::BeginTable("##WorkspaceTable", 3, workspace_table_flags)) { ImGui::TableSetupColumn("Tabs", ImGuiTableColumnFlags_WidthStretch, 0.9f); ImGui::TableSetupColumn("SubTabs", ImGuiTableColumnFlags_WidthStretch, 1.6f); ImGui::TableSetupColumn("Content", ImGuiTableColumnFlags_WidthStretch, 4.0f); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Tabs"); ImGui::Separator(); if (ImGui::BeginChild("##WorkspaceTabs", {0.0f, 0.0f}, false)) { for (auto mode : tab_order) { if (Tabs[mode].empty()) { continue; } bool selected = ActivePanelMode == mode; string name = !PanelModeNames[mode].empty() ? PanelModeNames[mode] : strex("Tab {}", mode).str(); if (ImGui::Selectable(name.c_str(), selected)) { SetActivePanelMode(mode); } } } ImGui::EndChild(); ImGui::TableNextColumn(); ImGui::Text("SubTabs"); ImGui::Separator(); if (ImGui::BeginChild("##WorkspaceSubTabs", {0.0f, 0.0f}, false)) { if (auto active_subtab = GetActiveSubTab()) { for (auto& [name, subtab] : Tabs[ActivePanelMode]) { bool selected = active_subtab == &subtab; if (ImGui::Selectable(name.c_str(), selected)) { ActiveSubTabs[ActivePanelMode] = &subtab; RefreshActiveProtoLists(); } } } else { ImGui::TextDisabled("No subtabs"); } } ImGui::EndChild(); ImGui::TableNextColumn(); if (IsItemMode() || IsCritMode()) { ImGuiInputTextStringWithHint("##WorkspaceFilter", "Filter prototypes...", WorkspaceFilter); string_view filter = WorkspaceFilter; ImGui::Separator(); if (ImGui::BeginChild("##WorkspaceProtoList", {0.0f, 0.0f}, false)) { auto draw_proto_entry = [&](int32_t index, string_view label, nptr<const Sprite> sprite, auto&& on_select) { ImGui::PushID(index); bool selected = index == GetActiveProtoIndex(); float32_t row_height = 72.0f; float32_t row_width = ImGui::GetContentRegionAvail().x; auto cursor = ImGui::GetCursorScreenPos(); bool clicked = ImGui::Selectable("##ProtoRow", selected, 0, {row_width, row_height}); auto rect_min = ImGui::GetItemRectMin(); auto rect_max = ImGui::GetItemRectMax(); auto draw_list = make_ptr(ImGui::GetWindowDrawList()); draw_list->AddRectFilled(rect_min, rect_max, ImGui::ColorConvertFloat4ToU32(selected ? ImVec4(0.24f, 0.32f, 0.18f, 0.65f) : ImVec4(0.08f, 0.08f, 0.08f, 0.35f)), 4.0f); draw_list->AddRect(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_Border), 4.0f); ImVec2 preview_min {cursor.x + 8.0f, cursor.y + 8.0f}; ImVec2 preview_max {cursor.x + 64.0f, cursor.y + 64.0f}; draw_list->AddRectFilled(preview_min, preview_max, ImGui::ColorConvertFloat4ToU32({0.12f, 0.12f, 0.12f, 1.0f}), 4.0f); if (auto atlas_sprite = ResolveAtlasSprite(sprite)) { FO_VERIFY_AND_THROW(sprite, "Resolved atlas sprite requires a source sprite"); isize32 sprite_size = sprite->GetSize(); float32_t scale = std::min(56.0f / std::max(1, sprite_size.width), 56.0f / std::max(1, sprite_size.height)); float32_t draw_width = numeric_cast<float32_t>(sprite_size.width) * scale; float32_t draw_height = numeric_cast<float32_t>(sprite_size.height) * scale; ImVec2 image_min {preview_min.x + (56.0f - draw_width) * 0.5f, preview_min.y + (56.0f - draw_height) * 0.5f}; DrawAtlasSpriteImage(draw_list, atlas_sprite, image_min, {draw_width, draw_height}); } draw_list->AddText({cursor.x + 76.0f, cursor.y + 10.0f}, ImGui::GetColorU32(ImGuiCol_Text), string(label).c_str()); if (clicked) { on_select(); } if (ImGui::IsItemHovered() && sprite) { if (ImGui::BeginTooltip()) { if (auto atlas_sprite = ResolveAtlasSprite(sprite)) { isize32 sprite_size = sprite->GetSize(); ImVec2 image_size {numeric_cast<float32_t>(std::max(1, sprite_size.width)), numeric_cast<float32_t>(std::max(1, sprite_size.height))}; ImVec2 image_min = ImGui::GetCursorScreenPos(); auto tooltip_draw_list = make_ptr(ImGui::GetWindowDrawList()); if (DrawAtlasSpriteImage(tooltip_draw_list, atlas_sprite, image_min, image_size)) { ImGui::Dummy(image_size); } else { ImGui::TextDisabled("No texture"); } } else { ImGui::TextDisabled("No preview"); } ImGui::EndTooltip(); } } ImGui::PopID(); }; if (IsItemMode() && ActiveItemProtos) { vector<int32_t> visible_indices; visible_indices.reserve(ActiveItemProtos->size()); for (int32_t i = 0; i < numeric_cast<int32_t>(ActiveItemProtos->size()); i++) { const auto& proto = (*ActiveItemProtos)[i]; if (ContainsCaseInsensitive(proto->GetName(), filter)) { visible_indices.emplace_back(i); } } ImGuiListClipper clipper; clipper.Begin(numeric_cast<int32_t>(visible_indices.size()), 72.0f); while (clipper.Step()) { for (int32_t row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) { int32_t i = visible_indices[row]; const auto& proto = (*ActiveItemProtos)[i]; string label = string(proto->GetName()); draw_proto_entry(i, label, GetPreviewSprite(proto->GetPicMap()), [&] { SetActiveProtoIndex(i); if (_curMap && ImGui::GetIO().KeyCtrl) { hstring pid = proto->GetProtoId(); auto& stab = Tabs[INT_MODE_IGNORE][DEFAULT_SUB_TAB]; bool found = false; for (auto it = stab.ItemProtos.begin(); it != stab.ItemProtos.end(); ++it) { if ((*it)->GetProtoId() == pid) { found = true; stab.ItemProtos.erase(it); break; } } if (!found) { stab.ItemProtos.emplace_back(proto); } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->SwitchIgnorePid(pid); cur_map->RebuildMap(); } else if (ImGui::GetIO().KeyAlt && !SelectedEntities.empty()) { bool add = true; if (proto->GetStackable()) { vector<refcount_ptr<ItemView>> children = GetEntityInnerItems(SelectedEntities.front()); for (size_t child_index = 0; child_index < children.size(); child_index++) { if (proto->GetProtoId() == children[child_index]->GetProtoId()) { add = false; break; } } } if (add) { CreateItem(proto->GetProtoId(), {}, SelectedEntities.front()); } } else { SetCurMode(CUR_MODE_PLACE_OBJECT); } }); } } } else if (IsCritMode() && ActiveCritterProtos) { vector<int32_t> visible_indices; visible_indices.reserve(ActiveCritterProtos->size()); for (int32_t i = 0; i < numeric_cast<int32_t>(ActiveCritterProtos->size()); i++) { const auto& proto = (*ActiveCritterProtos)[i]; if (ContainsCaseInsensitive(proto->GetName(), filter)) { visible_indices.emplace_back(i); } } ImGuiListClipper clipper; clipper.Begin(numeric_cast<int32_t>(visible_indices.size()), 72.0f); while (clipper.Step()) { for (int32_t row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) { int32_t i = visible_indices[row]; const auto& proto = (*ActiveCritterProtos)[i]; string label = string(proto->GetName()); auto preview_sprite = ResMngr.GetCritterPreviewSpr(proto->GetModelName(), CritterStateAnim::Unarmed, CritterActionAnim::Idle, CritterDir, nullptr); draw_proto_entry(i, label, preview_sprite, [&] { SetActiveProtoIndex(i); SetCurMode(CUR_MODE_PLACE_OBJECT); }); } } } } ImGui::EndChild(); } else { ImGui::Separator(); ImGui::TextDisabled("This mode uses the Content window."); if (ImGui::Button("Open Content")) { ContentWindowVisible = true; } } ImGui::EndTable(); } ImGui::End(); } void MapperEngine::DrawContentWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!ContentWindowVisible) { return; } ImGui::SetNextWindowPos( { numeric_cast<float32_t>(MainPanelPos.x + MainPanelContentRect.x), numeric_cast<float32_t>(MainPanelPos.y + MainPanelContentRect.y + 162), }, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({520.0f, 540.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("Content", &ContentWindowVisible, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::End(); return; } string& map_name_input = ContentMapName; string& map_filter = ContentMapFilter; int32_t& resize_w = ContentResizeW; int32_t& resize_h = ContentResizeH; auto run_button_action = [](bool triggered, auto&& action) { if (triggered) { action(); } }; auto run_button_action_with_message = [&](bool triggered, auto&& action, string_view message) { run_button_action(triggered, [&] { action(); AddMess(message); }); }; auto get_map_name_input = [&] { return strvex(map_name_input).trim().str(); }; if (ActivePanelMode == INT_MODE_INCONT) { if (!SelectedEntities.empty()) { vector<refcount_ptr<ItemView>> inner_items = GetEntityInnerItems(SelectedEntities.front()); ImGui::Text("Container items: %d", numeric_cast<int32_t>(inner_items.size())); if (ImGui::BeginChild("##ContainerItems", {0.0f, -ImGui::GetFrameHeightWithSpacing() * 2.0f}, true)) { for (size_t i = 0; i < inner_items.size(); i++) { auto inner_item = inner_items[i].as_ptr(); strex label = strex("{} x{}", inner_item->GetName(), inner_item->GetCount()); bool selected = InContItem == inner_item; if (ImGui::Selectable(label.c_str(), selected)) { InContItem = inner_items[i]; InspectorVisible = true; } } } ImGui::EndChild(); if (InContItem && ImGui::Button("Remove item")) { if (InContItem->GetOwnership() == ItemOwnership::CritterInventory) { if (_curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (auto owner = cur_map->GetCritter(InContItem->GetCritterId())) { owner->DeleteInvItem(InContItem); } } } else if (InContItem->GetOwnership() == ItemOwnership::ItemContainer) { if (_curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (auto owner = cur_map->GetItem(InContItem->GetContainerId())) { owner->DestroyInnerItem(InContItem); } } } InContItem.reset(); auto next_entity = SelectedEntities.empty() ? nptr<ClientEntity> {} : nptr<ClientEntity> {SelectedEntities.front()}; if (next_entity) { SelectClear(); SelectAdd(next_entity); } } if (InContItem && !SelectedEntities.empty()) { if (auto cr = SelectedEntities.front().dyn_cast<CritterHexView>()) { ImGui::SameLine(); if (ImGui::Button("Next slot")) { size_t to_slot = static_cast<size_t>(InContItem->GetCritterSlot()) + 1; while (numeric_cast<size_t>(to_slot) >= Settings->CritterSlotEnabled.size() || !Settings->CritterSlotEnabled[to_slot % 256]) { to_slot++; } to_slot %= 256; span<refcount_ptr<ItemView>> inv_items = cr->GetInvItems(); for (size_t i = 0; i < inv_items.size(); i++) { auto item = inv_items[i].as_ptr(); if (item->GetCritterSlot() == static_cast<CritterItemSlot>(to_slot)) { item->SetCritterSlot(CritterItemSlot::Inventory); } } InContItem->SetCritterSlot(static_cast<CritterItemSlot>(to_slot)); cr->RefreshView(); } } } } else { ImGui::TextDisabled("Select an entity with inventory or container items."); } } else if (ActivePanelMode == INT_MODE_LIST) { if (_curMap && (resize_w <= 0 || resize_h <= 0)) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); msize map_size = cur_map->GetSize(); resize_w = map_size.width; resize_h = map_size.height; } ImGuiInputTextStringWithHint("##MapFilter", "Filter maps...", map_filter); if (ImGui::BeginChild("##LoadedMaps", {0.0f, 180.0f}, true)) { for (auto& map : LoadedMaps) { string label = string(map->GetName()); if (!ContainsCaseInsensitive(label, map_filter)) { continue; } bool is_current = GetCurMap() == map; if (is_current) { label = strex("* {}", label); } if (ImGui::Selectable(label.c_str(), is_current) && !is_current) { ShowMap(map); } } } ImGui::EndChild(); ImGui::SeparatorText("Map commands"); ImGuiInputTextString("Map name", map_name_input); run_button_action(ImGui::Button("New map"), [&] { ParseCommand("* new"); }); ImGui::SameLine(); run_button_action(ImGui::Button("Unload current") && _curMap, [&] { ParseCommand("* unload"); }); ImGui::SameLine(); run_button_action(ImGui::Button("Resave all"), [&] { ParseCommand("* resave"); }); if (ImGui::Button("Load")) { string map_name = get_map_name_input(); if (!map_name.empty()) { if (auto map = LoadMap(map_name)) { ShowMap(map); AddMess("Load map success"); } else { AddMess("Load map failed"); } } } ImGui::SameLine(); run_button_action_with_message( ImGui::Button("Save current") && _curMap, [&] { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); SaveMap(cur_map, ""); }, "Save map success"); ImGui::SameLine(); if (ImGui::Button("Save As") && _curMap) { string map_name = get_map_name_input(); if (!map_name.empty()) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); SaveMap(cur_map, map_name); AddMess("Save map success"); } } ImGui::InputInt("Resize width", &resize_w); ImGui::InputInt("Resize height", &resize_h); if (ImGui::Button("Resize current") && _curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); ResizeMap(cur_map, resize_w, resize_h); AddMess(strex("Resize map to {}x{}", resize_w, resize_h)); } } else if (ActivePanelMode == INT_MODE_MESS) { if (ImGui::Button("Clear")) { MessBox.clear(); } ImGui::SameLine(); if (ImGui::Button("Copy all")) { string all_messages; for (const auto& entry : MessBox) { all_messages += entry.Time; all_messages += entry.Mess; } GetApp()->Input.SetClipboardText(all_messages); } ImGui::Separator(); if (ImGui::BeginChild("##Messages", {0.0f, 0.0f}, true)) { for (auto it = MessBox.rbegin(); it != MessBox.rend(); ++it) { ImGui::TextUnformatted(strex("{}{}", it->Time, it->Mess).c_str()); } } ImGui::EndChild(); } else { ImGui::TextDisabled("No content window for the current mode."); } ImGui::End(); } void MapperEngine::DrawCritterAnimationsWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!CritterAnimationsWindowVisible) { return; } ImGui::SetNextWindowPos( { numeric_cast<float32_t>(MainPanelPos.x + MainPanelContentRect.x + MainPanelContentRect.width + 24), numeric_cast<float32_t>(MainPanelPos.y + MainPanelContentRect.y + 132), }, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({360.0f, 160.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("Critter Animations", &CritterAnimationsWindowVisible, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::End(); return; } int32_t& anim_state = CritterAnimState; int32_t& anim_action = CritterAnimAction; string& anim_sequence = CritterAnimSequence; ImGui::InputInt("State", &anim_state); ImGui::InputInt("Action", &anim_action); if (ImGui::Button("Play pair")) { ParseCommand(strex("@ {} {}", anim_state, anim_action)); } ImGuiInputTextStringWithHint("##AnimSequence", "Sequence: state action [state action]...", anim_sequence); if (ImGui::Button("Play sequence")) { string sequence = strvex(anim_sequence).trim().str(); if (!sequence.empty()) { ParseCommand(strex("@ {}", sequence)); } } ImGui::End(); } void MapperEngine::DrawScriptCallWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!ScriptCallWindowVisible) { return; } ImGui::SetNextWindowPos( { numeric_cast<float32_t>(MainPanelPos.x + MainPanelContentRect.x + MainPanelContentRect.width + 24), numeric_cast<float32_t>(MainPanelPos.y + MainPanelContentRect.y + 304), }, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({380.0f, 140.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("Script Call", &ScriptCallWindowVisible, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::End(); return; } string& script_func = ScriptCallFunc; string& script_args = ScriptCallArgs; ImGuiInputTextStringWithHint("##ScriptFunc", "Function name", script_func); ImGuiInputTextStringWithHint("##ScriptArgs", "Arguments", script_args); if (ImGui::Button("Run script")) { string func_name = strvex(script_func).trim().str(); string args = strvex(script_args).trim().str(); if (!func_name.empty()) { if (!args.empty()) { ParseCommand(strex("#{} {}", func_name, args)); } else { ParseCommand(strex("#{}", func_name)); } } } ImGui::End(); } void MapperEngine::DrawMapListWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!MapListWindowVisible) { return; } auto viewport = make_ptr(ImGui::GetMainViewport()); ImGui::SetNextWindowPos( { viewport->GetCenter().x, viewport->GetCenter().y, }, ImGuiCond_Appearing, {0.5f, 0.5f}); ImGui::SetNextWindowSize({380.0f, 420.0f}, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSizeConstraints({380.0f, 420.0f}, {std::numeric_limits<float32_t>::max(), std::numeric_limits<float32_t>::max()}); if (!ImGui::Begin("Map Browser", &MapListWindowVisible, 0)) { ImGui::End(); return; } string& map_filter = MapBrowserFilter; if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); MapBrowserNamesStale = true; } ImGuiInputTextStringWithHint("##AllMapsFilter", "Search maps...", map_filter); if (MapBrowserNamesStale) { MapBrowserNames.clear(); auto map_files = MapsFileSys.FilterFiles(""); for (const auto& map_file_header : map_files) { if (!IsProtoFileExtension(map_file_header.GetPath())) { continue; } File map_file = File::Load(map_file_header); auto declared_maps = MapLoader::EnumerateMaps(map_file.GetPath(), map_file.GetStr()); MapBrowserNames.insert(MapBrowserNames.end(), std::make_move_iterator(declared_maps.begin()), std::make_move_iterator(declared_maps.end())); } std::ranges::sort(MapBrowserNames); MapBrowserNamesStale = false; } vector<string> map_names; map_names.reserve(MapBrowserNames.size()); for (const auto& map_name : MapBrowserNames) { if (!ContainsCaseInsensitive(map_name, map_filter)) { continue; } map_names.emplace_back(map_name); } ImGui::Text("Maps: %d", numeric_cast<int32_t>(map_names.size())); ImGui::Separator(); if (ImGui::BeginChild("##AllMapsList", {0.0f, 0.0f}, true)) { for (const auto& map_name : map_names) { auto loaded_it = std::ranges::find_if(LoadedMaps, [&](const auto& map) { return string(map->GetName()) == map_name; }); bool is_loaded = loaded_it != LoadedMaps.end(); auto loaded_map = is_loaded ? nptr<MapView> {*loaded_it} : nptr<MapView> {}; bool is_current = is_loaded && GetCurMap() == loaded_map; auto label = map_name; if (is_current) { label = strex("* {}", label).str(); } else if (is_loaded) { label = strex("+ {}", label).str(); } if (ImGui::Selectable(label.c_str(), is_current)) { if (is_loaded) { ShowMap(*loaded_it); } else if (auto map = LoadMap(map_name); map) { ShowMap(map); AddMess(strex("Load map success: {}", map_name)); } else { AddMess(strex("Load map failed: {}", map_name)); } } } } ImGui::EndChild(); ImGui::End(); } void MapperEngine::DrawMapWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!MapWindowVisible || !_curMap) { return; } auto viewport = make_ptr(ImGui::GetMainViewport()); ImGui::SetNextWindowPos( { viewport->WorkPos.x + viewport->WorkSize.x - 16.0f, viewport->WorkPos.y + MainPanelWindowRect.height + 8.0f, }, ImGuiCond_FirstUseEver, {1.0f, 0.0f}); if (!ImGui::Begin("Controls", &MapWindowVisible, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::End(); return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); mpos hex; cur_map->GetHexAtScreen(MousePos, hex, nullptr); int32_t day_time = GetGlobalDayTime(); string map_name = string(cur_map->GetName()); auto rotate_selected_critters = [&] { for (size_t i = 0; i < SelectedEntities.size(); i++) { if (auto cr = SelectedEntities[i].dyn_cast<CritterHexView>()) { AdvanceCritterDir(cr); } } }; if (ImGui::BeginTable("##ControlsSummary", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); auto draw_summary_row = [](string_view label, auto&& draw_value) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGuiTextUnformatted(label); ImGui::TableNextColumn(); draw_value(); }; draw_summary_row("Map", [&] { ImGui::TextUnformatted(map_name.c_str()); }); draw_summary_row("Mouse hex", [&] { ImGui::Text("%d, %d", hex.x, hex.y); }); draw_summary_row("Time", [&] { ImGui::Text("%02d:%02d", day_time / 60 % 24, day_time % 60); }); draw_summary_row("FPS", [&] { ImGui::Text("%d", GameTime.GetFramesPerSecond()); }); draw_summary_row("Tile layer", [&] { ImGui::Text("%d", TileLayer); }); ImGui::EndTable(); } ImGui::SeparatorText("Map"); if (ImGui::Button("Time +1h")) { SetGlobalDayTime(ShiftDayTimeWithWrap(day_time, 60)); } ImGui::SameLine(); if (ImGui::Button("Time -1h")) { SetGlobalDayTime(ShiftDayTimeWithWrap(day_time, -60)); } ImGui::SameLine(); if (ImGui::Button("Toggle hex")) { ToggleMapperHexOverlay(); } float32_t current_zoom = cur_map->GetSpritesZoomTarget(); ImGui::Text("Zoom: %.2f", current_zoom); if (ImGui::Button("Zoom in")) { ChangeZoom(ScaleZoomValue(current_zoom, 1.2f)); } ImGui::SameLine(); if (ImGui::Button("Zoom out")) { ChangeZoom(ScaleZoomValue(current_zoom, 0.8f)); } ImGui::SameLine(); if (ImGui::Button("Zoom reset")) { ChangeZoom(1.0f); } int32_t tile_layer = TileLayer; ImGui::SetNextItemWidth(140.0f); if (ImGui::SliderInt("##TileLayer", &tile_layer, 0, 4)) { TileLayer = tile_layer; } ImGui::SameLine(); ImGui::TextUnformatted("Tile layer"); bool scroll_check_enabled = cur_map->IsScrollCheck(); if (ImGui::Checkbox("Scroll check", &scroll_check_enabled)) { cur_map->SetScrollCheck(scroll_check_enabled); } ImGui::Checkbox("Roof preview", &PreviewRoofTiles); string rotate_preview_dir_label = strex("Rotate preview dir ({})", CritterDir).str(); if (ImGui::Button(rotate_preview_dir_label.c_str())) { CritterDir = GetNextCritterDir(CritterDir); } if (ImGui::Button("Rotate selected critters")) { rotate_selected_critters(); } ImGui::Checkbox("Axial grid selection", &SelectAxialGrid); ImGui::Checkbox("Select entire entity", &SelectEntireEntity); auto visibility_before = array { Settings->ShowItem, Settings->ShowScen, Settings->ShowWall, Settings->ShowCrit, Settings->ShowTile, Settings->ShowRoof, Settings->ShowFast, }; auto draw_checkbox_group = [](auto&& entries) { for (const auto& [label, value] : entries) { ImGui::Checkbox(label, value); } }; if (ImGui::CollapsingHeader("Visibility")) { draw_checkbox_group(array { std::pair {"Items", &Settings->ShowItem}, std::pair {"Scenery", &Settings->ShowScen}, std::pair {"Walls", &Settings->ShowWall}, std::pair {"Critters", &Settings->ShowCrit}, std::pair {"Tiles", &Settings->ShowTile}, std::pair {"Roof", &Settings->ShowRoof}, std::pair {"Fast", &Settings->ShowFast}, }); } auto visibility_after = array { Settings->ShowItem, Settings->ShowScen, Settings->ShowWall, Settings->ShowCrit, Settings->ShowTile, Settings->ShowRoof, Settings->ShowFast, }; bool visibility_changed = visibility_before != visibility_after; if (visibility_changed) { cur_map->RebuildMap(); } if (ImGui::CollapsingHeader("Selection")) { draw_checkbox_group(array { std::pair {"Select items", &SelectItemsEnabled}, std::pair {"Select scenery", &SelectSceneryEnabled}, std::pair {"Select walls", &SelectWallsEnabled}, std::pair {"Select critters", &SelectCrittersEnabled}, std::pair {"Select tiles", &SelectTilesEnabled}, std::pair {"Select roof", &SelectRoofTilesEnabled}, }); } ImGui::End(); } void MapperEngine::DrawInspectorImGui() { FO_STACK_TRACE_ENTRY(); if (!InspectorVisible) { return; } auto entity = GetInspectorEntity(); if (!entity) { return; } auto item = entity.dyn_cast<ItemView>(); auto cr = entity.dyn_cast<CritterView>(); auto entity_with_proto = entity.dyn_cast<EntityWithProto>(); FO_VERIFY_AND_THROW(entity_with_proto, "Inspected entity does not have an associated prototype"); string_view type_name = cr ? "Critter" : (item && !item->GetStatic() ? "Dynamic Item" : "Static Item"); ImGui::SetNextWindowPos({numeric_cast<float32_t>(InspectorPos.x), numeric_cast<float32_t>(InspectorPos.y)}, ImGuiCond_Once); ImGui::SetNextWindowSize({380.0f, 520.0f}, ImGuiCond_Once); bool keep_open = InspectorVisible; auto flags = ImGuiWindowFlags_AlwaysAutoResize; if (!ImGui::Begin("Inspector", &keep_open, flags)) { ImGui::End(); if (!keep_open) { SelectClear(); } return; } auto pos = ImGui::GetWindowPos(); InspectorPos = {iround<int32_t>(pos.x), iround<int32_t>(pos.y)}; int32_t compatible_candidates = 0; auto is_same_inspector_entity_type = [&](ptr<const ClientEntity> selected_entity) { auto selected_cr = selected_entity.dyn_cast<CritterView>(); auto selected_item = selected_entity.dyn_cast<ItemView>(); bool same_cr = selected_cr && cr; bool same_item = selected_item && item; return same_cr || same_item; }; auto is_inspector_front_entity = [&]() -> bool { if (SelectedEntities.empty()) { return false; } nptr<ClientEntity> front_entity = SelectedEntities.front(); nptr<const ClientEntity> inspected_entity = entity; return front_entity == inspected_entity; }; if (SelectedEntities.size() > 1 && is_inspector_front_entity()) { for (size_t i = 0; i < SelectedEntities.size(); i++) { if (is_same_inspector_entity_type(SelectedEntities[i])) { compatible_candidates++; } } } bool can_apply_to_all = compatible_candidates > 1; if (!can_apply_to_all) { InspectorApplyToAll = false; } if (can_apply_to_all) { string apply_to_all_label = strex("Apply to all ({})", compatible_candidates).str(); ImGui::Checkbox(apply_to_all_label.c_str(), &InspectorApplyToAll); } constexpr int32_t START_LINE = 3; string& edit_buf = InspectorEditBuf; int32_t& edit_line = InspectorEditLine; int32_t& pending_focus_line = InspectorPendingFocusLine; int32_t& pending_focus_array_index = InspectorPendingFocusArrayIndex; int32_t& pending_caret_reset_line = InspectorPendingCaretResetLine; int32_t& pending_caret_reset_array_index = InspectorPendingCaretResetArrayIndex; int32_t& pending_caret_reset_frames = InspectorPendingCaretResetFrames; bool& last_edit_cell_rect_valid = InspectorLastEditCellRectValid; float32_t& last_edit_cell_min_x = InspectorLastEditCellMinX; float32_t& last_edit_cell_min_y = InspectorLastEditCellMinY; float32_t& last_edit_cell_max_x = InspectorLastEditCellMaxX; float32_t& last_edit_cell_max_y = InspectorLastEditCellMaxY; auto left_column_bg = ImGui::ColorConvertFloat4ToU32({1.0f, 1.0f, 1.0f, 0.10f}); auto readonly_value_bg = ImGui::ColorConvertFloat4ToU32({0.72f, 0.78f, 0.88f, 0.22f}); auto same_as_proto_bg = ImGui::ColorConvertFloat4ToU32({1.0f, 1.0f, 1.0f, 0.10f}); auto changed_from_proto_bg = ImGui::ColorConvertFloat4ToU32({0.55f, 0.55f, 0.55f, 0.22f}); auto sync_edit_buf = [&]() { edit_buf = InspectorSelectedLineValue; if (edit_buf.capacity() < 4096) { edit_buf.reserve(4096); } }; auto clear_edit_state = [&]() { ResetInspectorPropertyEditState(); }; auto begin_edit_state = [&](int32_t line, int32_t array_index = 0) { edit_line = line; sync_edit_buf(); pending_focus_line = line; pending_focus_array_index = array_index; pending_caret_reset_line = line; pending_caret_reset_array_index = array_index; pending_caret_reset_frames = 2; }; auto reset_selected_line_state = [&](int32_t selected_line) { SelectInspectorPropertyLine(selected_line); sync_edit_buf(); clear_edit_state(); }; auto keep_selected_line_edit_state = [&](int32_t selected_line) { SelectInspectorPropertyLine(selected_line); sync_edit_buf(); edit_line = selected_line; }; auto restore_selected_line_initial_value = [&] { CancelInspectorPropertyEdit(); }; auto apply_to_compatible_selected_entities = [&](auto&& action) { if (!(InspectorApplyToAll && can_apply_to_all && is_inspector_front_entity())) { return; } for (size_t j = 1; j < SelectedEntities.size(); j++) { auto selected_entity = SelectedEntities[j].as_ptr(); if (is_same_inspector_entity_type(selected_entity)) { action(selected_entity); } } }; if (edit_line != -1 && last_edit_cell_rect_valid && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { auto mouse_pos = ImGui::GetMousePos(); bool click_inside_edit_cell = mouse_pos.x >= last_edit_cell_min_x && mouse_pos.x <= last_edit_cell_max_x && mouse_pos.y >= last_edit_cell_min_y && mouse_pos.y <= last_edit_cell_max_y; if (!click_inside_edit_cell) { CancelInspectorPropertyEdit(); } } ImGui::Separator(); // Preserve inspector keyboard navigation when the window has focus. if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive()) { if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) { reset_selected_line_state(InspectorSelectedLine - 1); } else if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) { reset_selected_line_state(InspectorSelectedLine + 1); } else if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { restore_selected_line_initial_value(); } } auto apply_value = [&](ptr<Entity> target_entity) { ApplyInspectorPropertyEdit(target_entity); }; auto apply_selected_value = [&](int32_t selected_line) { apply_value(entity); apply_to_compatible_selected_entities(apply_value); reset_selected_line_state(selected_line); }; auto apply_selected_value_keep_edit = [&](int32_t selected_line) { apply_value(entity); apply_to_compatible_selected_entities(apply_value); keep_selected_line_edit_state(selected_line); }; auto reset_selected_value = [&](int32_t selected_line) { if (selected_line < START_LINE || selected_line - START_LINE >= numeric_cast<int32_t>(ShowProps.size())) { return; } auto selected_prop = ShowProps[selected_line - START_LINE]; if (!selected_prop) { return; } try { InspectorSelectedLineValue = entity_with_proto->GetProto()->GetProperties()->SavePropertyToText(selected_prop); apply_value(entity); } catch (const std::exception&) { } reset_selected_line_state(selected_line); }; if (ImGui::BeginChild("##InspectorProps", {0.0f, 0.0f}, false)) { if (ImGui::BeginTable("##InspectorGrid", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 170.0f); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); int32_t focus_edit_line = pending_focus_line; int32_t focus_edit_array_index = pending_focus_array_index >= 0 ? pending_focus_array_index : 0; int32_t caret_reset_array_index = pending_caret_reset_array_index >= 0 ? pending_caret_reset_array_index : 0; auto draw_summary_row = [&](string_view label, string_view value) { ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFrameHeightWithSpacing() + 2.0f); ImGui::TableNextColumn(); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, left_column_bg); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(string(label).c_str()); ImGui::TableNextColumn(); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, readonly_value_bg); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(string(value).c_str()); }; draw_summary_row("Type", type_name); draw_summary_row("Id", strex("{}", entity->GetId()).str()); draw_summary_row("ProtoId", entity_with_proto->GetProtoId()); for (size_t i = 0; i < ShowProps.size(); i++) { int32_t line = START_LINE + numeric_cast<int32_t>(i); auto prop = ShowProps[i]; ImGui::PushID(line); if (!prop) { ImGui::TableNextRow(ImGuiTableRowFlags_None, 1.0f); ImGui::TableNextColumn(); ImGui::Separator(); ImGui::TableNextColumn(); ImGui::Separator(); ImGui::PopID(); continue; } ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFrameHeightWithSpacing() + 2.0f); string value = entity->GetProperties()->SavePropertyToText(prop); bool is_const = prop->IsCoreProperty() && !prop->IsMutable(); bool selected = InspectorSelectedLine == line; bool same_as_proto = IsInspectorValueSameAsProto(entity, prop, value); string label = strex("{} ({})", prop->GetName(), prop->GetViewTypeName()).str(); ImGui::TableNextColumn(); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, left_column_bg); if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) { reset_selected_line_state(line); } ImGui::TableNextColumn(); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, is_const ? readonly_value_bg : (same_as_proto ? same_as_proto_bg : changed_from_proto_bg)); auto value_cell_start = ImGui::GetCursorScreenPos(); float32_t value_cell_width = ImGui::GetContentRegionAvail().x; bool request_focus = focus_edit_line == line; bool request_caret_reset = pending_caret_reset_line == line && pending_caret_reset_frames > 0; auto select_value_line = [&] { SelectInspectorPropertyLine(line); if (!is_const) { begin_edit_state(line); } else { reset_selected_line_state(line); } }; if (selected && !is_const && edit_line == line) { if (pending_focus_line == line) { focus_edit_line = line; } auto struct_layout = GetInspectorStructLayout(prop); bool commit_requested = false; bool cancel_requested = false; bool finish_edit_requested = false; bool focus_consumed = false; auto edit_text_value = [&](float32_t item_width = -58.0f) { ImGui::SetNextItemWidth(item_width); if (request_focus) { ImGui::SetKeyboardFocusHere(); focus_consumed = true; } bool value_submitted = ImGuiInputTextString("##value", edit_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, request_caret_reset); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); InspectorSelectedLineValue = edit_buf; commit_requested = value_submitted || value_deactivated; finish_edit_requested = value_submitted; cancel_requested = ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); }; auto edit_struct_fields = [&](vector<string>& field_values, int32_t struct_id, bool request_focus, bool request_caret_reset) { bool struct_changed = false; for (size_t field_index = 0; field_index < field_values.size(); field_index++) { const auto& field = struct_layout->Fields[field_index]; ImGui::PushID(strex("{}:{}", struct_id, field.Name).str().c_str()); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(field.Name.c_str()); ImGui::SameLine(); ImGui::SetNextItemWidth(-1.0f); if (request_focus && field_index == 0) { ImGui::SetKeyboardFocusHere(); focus_consumed = true; } if (field.Type.IsBool) { bool current_value = false; try { current_value = AnyData::ParseValue(field_values[field_index], false, false, AnyData::ValueType::Bool).AsBool(); } catch (const std::exception&) { } if (ImGui::Checkbox("##field", ¤t_value)) { field_values[field_index] = AnyData::ValueToString(AnyData::Value {current_value}); struct_changed = true; commit_requested = true; } } else if (field.Type.IsInt) { auto field_buf = field_values[field_index]; if (field_buf.capacity() < 512) { field_buf.reserve(512); } bool value_submitted = ImGuiInputTextString("##field", field_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, request_caret_reset && field_index == 0); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); field_values[field_index] = std::move(field_buf); struct_changed |= value_submitted || value_deactivated; commit_requested |= value_submitted || value_deactivated; finish_edit_requested |= value_submitted; cancel_requested |= ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); } else if (field.Type.IsFloat) { auto field_buf = field_values[field_index]; if (field_buf.capacity() < 512) { field_buf.reserve(512); } bool value_submitted = ImGuiInputTextString("##field", field_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, request_caret_reset && field_index == 0); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); field_values[field_index] = std::move(field_buf); struct_changed |= value_submitted || value_deactivated; commit_requested |= value_submitted || value_deactivated; finish_edit_requested |= value_submitted; cancel_requested |= ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); } else { auto field_buf = field_values[field_index]; if (field_buf.capacity() < 512) { field_buf.reserve(512); } bool value_submitted = ImGuiInputTextString("##field", field_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, request_caret_reset && field_index == 0); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); field_values[field_index] = std::move(field_buf); struct_changed |= value_submitted || value_deactivated; commit_requested |= value_submitted || value_deactivated; finish_edit_requested |= value_submitted; cancel_requested |= ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); } ImGui::PopID(); } return struct_changed; }; if (prop->IsArray() && struct_layout) { if (auto parsed_entries = ParseInspectorStringEntries(InspectorSelectedLineValue); parsed_entries.has_value()) { auto struct_entries = *parsed_entries; bool array_changed = false; std::optional<size_t> remove_index {}; for (size_t entry_index = 0; entry_index < struct_entries.size(); entry_index++) { auto field_values = ParseInspectorStructFields(*struct_layout, struct_entries[entry_index]).value_or(vector<string>(struct_layout->Fields.size())); ImGui::PushID(numeric_cast<int32_t>(entry_index)); ImGui::SeparatorText(strex("Item {}", entry_index).str().c_str()); array_changed |= edit_struct_fields(field_values, numeric_cast<int32_t>(entry_index), focus_edit_line == line && entry_index == numeric_cast<size_t>(focus_edit_array_index), pending_caret_reset_line == line && pending_caret_reset_frames > 0 && entry_index == numeric_cast<size_t>(caret_reset_array_index)); struct_entries[entry_index] = SerializeInspectorStringArray(field_values); if (ImGui::SmallButton("Remove")) { remove_index = entry_index; commit_requested = true; } ImGui::PopID(); } if (remove_index.has_value()) { struct_entries.erase(struct_entries.begin() + numeric_cast<ptrdiff_t>(*remove_index)); array_changed = true; } if (ImGui::Button("Add element")) { struct_entries.emplace_back(SerializeInspectorStringArray(vector<string>(struct_layout->Fields.size()))); array_changed = true; pending_focus_line = line; pending_focus_array_index = numeric_cast<int32_t>(struct_entries.size()) - 1; pending_caret_reset_line = line; pending_caret_reset_array_index = pending_focus_array_index; pending_caret_reset_frames = 2; } if (array_changed) { InspectorSelectedLineValue = SerializeInspectorStringArray(struct_entries); } } } else if (prop->IsArray()) { if (auto parsed_value = ParseInspectorValue(prop, InspectorSelectedLineValue); parsed_value.has_value() && parsed_value->Type() == AnyData::ValueType::Array) { vector<AnyData::Value> entries; entries.reserve(parsed_value->AsArray().Size()); for (const auto& entry : parsed_value->AsArray()) { entries.emplace_back(entry.Copy()); } bool array_changed = false; std::optional<size_t> remove_index {}; for (size_t entry_index = 0; entry_index < entries.size(); entry_index++) { ImGui::PushID(numeric_cast<int32_t>(entry_index)); if (focus_edit_line == line && entry_index == numeric_cast<size_t>(focus_edit_array_index)) { ImGui::SetKeyboardFocusHere(); focus_consumed = true; } switch (GetInspectorValueType(prop)) { case AnyData::ValueType::Int64: { string value_buf = AnyData::ValueToString(entries[entry_index]); if (value_buf.capacity() < 512) { value_buf.reserve(512); } ImGui::SetNextItemWidth(-34.0f); bool value_submitted = ImGuiInputTextString("##ArrayValue", value_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, pending_caret_reset_line == line && pending_caret_reset_frames > 0 && entry_index == numeric_cast<size_t>(caret_reset_array_index)); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); if (value_submitted || value_deactivated) { try { int64_t entry_value = AnyData::ParseValue(value_buf, false, false, AnyData::ValueType::Int64).AsInt64(); if (!prop->IsBaseTypeSignedInt() && entry_value < 0) { entry_value = 0; } entries[entry_index] = AnyData::Value {entry_value}; array_changed = true; } catch (const std::exception&) { } } commit_requested |= value_submitted || value_deactivated; finish_edit_requested |= value_submitted; cancel_requested |= ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); break; } case AnyData::ValueType::Float64: { string value_buf = AnyData::ValueToString(entries[entry_index]); if (value_buf.capacity() < 512) { value_buf.reserve(512); } ImGui::SetNextItemWidth(-34.0f); bool value_submitted = ImGuiInputTextString("##ArrayValue", value_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, pending_caret_reset_line == line && pending_caret_reset_frames > 0 && entry_index == numeric_cast<size_t>(caret_reset_array_index)); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); if (value_submitted || value_deactivated) { try { entries[entry_index] = AnyData::Value {AnyData::ParseValue(value_buf, false, false, AnyData::ValueType::Float64).AsDouble()}; array_changed = true; } catch (const std::exception&) { } } commit_requested |= value_submitted || value_deactivated; finish_edit_requested |= value_submitted; cancel_requested |= ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); break; } case AnyData::ValueType::Bool: { bool entry_value = entries[entry_index].AsBool(); if (ImGui::Checkbox("##ArrayValue", &entry_value)) { entries[entry_index] = AnyData::Value {entry_value}; array_changed = true; commit_requested = true; } break; } case AnyData::ValueType::String: { string string_buf = string(entries[entry_index].AsString()); if (string_buf.capacity() < 512) { string_buf.reserve(512); } ImGui::SetNextItemWidth(-34.0f); bool value_submitted = ImGuiInputTextString("##ArrayValue", string_buf, ImGuiInputTextFlags_EnterReturnsTrue, true, pending_caret_reset_line == line && pending_caret_reset_frames > 0 && entry_index == numeric_cast<size_t>(caret_reset_array_index)); bool value_deactivated = ImGui::IsItemDeactivatedAfterEdit(); if (value_submitted || value_deactivated) { entries[entry_index] = AnyData::Value {std::move(string_buf)}; array_changed = true; } commit_requested |= value_submitted || value_deactivated; finish_edit_requested |= value_submitted; cancel_requested |= ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); break; } default: FO_UNREACHABLE_PLACE(); } ImGui::SameLine(); if (ImGui::SmallButton("X")) { remove_index = entry_index; commit_requested = true; } ImGui::PopID(); } if (remove_index.has_value()) { entries.erase(entries.begin() + numeric_cast<ptrdiff_t>(*remove_index)); array_changed = true; } if (ImGui::Button("Add element")) { entries.emplace_back(MakeDefaultInspectorArrayElement(prop)); array_changed = true; pending_focus_line = line; pending_focus_array_index = numeric_cast<int32_t>(entries.size()) - 1; pending_caret_reset_line = line; pending_caret_reset_array_index = pending_focus_array_index; pending_caret_reset_frames = 2; } if (array_changed) { InspectorSelectedLineValue = SerializeInspectorArray(std::move(entries)); } } else { edit_text_value(); } } else if (struct_layout) { auto field_values = ParseInspectorStructFields(*struct_layout, InspectorSelectedLineValue).value_or(vector<string>(struct_layout->Fields.size())); if (edit_struct_fields(field_values, -1, focus_edit_line == line, pending_caret_reset_line == line && pending_caret_reset_frames > 0)) { InspectorSelectedLineValue = SerializeInspectorStringArray(field_values); } } else if (prop->IsBaseTypeBool()) { bool current_value = false; if (auto parsed_value = ParseInspectorValue(prop, InspectorSelectedLineValue); parsed_value.has_value() && parsed_value->Type() == AnyData::ValueType::Bool) { current_value = parsed_value->AsBool(); } if (focus_edit_line == line) { ImGui::SetKeyboardFocusHere(); focus_consumed = true; } if (ImGui::Checkbox("##value", ¤t_value)) { InspectorSelectedLineValue = AnyData::ValueToString(AnyData::Value {current_value}); commit_requested = true; } } else if (prop->IsBaseTypeInt() && !prop->IsBaseTypeSimpleStruct()) { edit_text_value(); } else if (prop->IsBaseTypeFloat() && !prop->IsBaseTypeSimpleStruct()) { edit_text_value(); } else { edit_text_value(); } cancel_requested |= ImGui::IsKeyPressed(ImGuiKey_Escape, false); last_edit_cell_min_x = value_cell_start.x; last_edit_cell_min_y = value_cell_start.y; last_edit_cell_max_x = value_cell_start.x + value_cell_width; last_edit_cell_max_y = std::max(ImGui::GetCursorScreenPos().y, value_cell_start.y + ImGui::GetFrameHeightWithSpacing()); last_edit_cell_rect_valid = true; if (pending_caret_reset_line == line && pending_caret_reset_frames > 0) { pending_caret_reset_frames--; if (pending_caret_reset_frames == 0) { pending_caret_reset_line = -1; pending_caret_reset_array_index = -1; } } if (cancel_requested) { restore_selected_line_initial_value(); } else if (commit_requested) { if (finish_edit_requested) { apply_selected_value(line); } else { apply_selected_value_keep_edit(line); } } if (focus_consumed && pending_focus_line == line) { pending_focus_line = -1; pending_focus_array_index = -1; } } else { if (selected) { if (!is_const) { float32_t reset_label_width = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2.0f; bool reset_enabled = !same_as_proto; float32_t value_button_width = reset_enabled ? std::max(1.0f, ImGui::GetContentRegionAvail().x - reset_label_width - ImGui::GetStyle().ItemSpacing.x) : ImGui::GetContentRegionAvail().x; if (ImGui::Selectable(strex("{}##value_display", value).str().c_str(), false, ImGuiSelectableFlags_AllowOverlap, {value_button_width, 0.0f})) { begin_edit_state(line); } if (reset_enabled) { ImGui::SameLine(); if (ImGui::SmallButton("Reset")) { reset_selected_value(line); } } } else { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(value.c_str()); } } else { if (ImGui::Selectable(strex("{}##value_select", value).str().c_str(), false, ImGuiSelectableFlags_AllowOverlap, {ImGui::GetContentRegionAvail().x, 0.0f})) { select_value_line(); } } } ImGui::PopID(); } if (edit_line == -1) { last_edit_cell_rect_valid = false; } ImGui::EndTable(); } } ImGui::EndChild(); ImGui::End(); if (!keep_open) { SelectClear(); } } void MapperEngine::ApplyInspectorPropertyEdit(ptr<Entity> entity) { FO_STACK_TRACE_ENTRY(); constexpr int32_t start_line = 3; if (InspectorSelectedLine >= start_line && InspectorSelectedLine - start_line < numeric_cast<int32_t>(ShowProps.size())) { const auto& prop = ShowProps[InspectorSelectedLine - start_line]; if (prop) { ident_t entity_id = entity->GetId(); string old_value = InspectorSelectedLineInitialValue; string new_value = InspectorSelectedLineValue; if (!ApplyEntityPropertyText(entity, prop, new_value)) { ApplyEntityPropertyText(entity, prop, old_value); return; } string prop_name = string(prop->GetName()); PushUndoOp(GetCurMap(), UndoOp {strex("Edit {}", prop_name), [entity_id, prop_name, old_value](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { auto target = mapper->FindEntityById(*active_map, entity_id); if (!target) { return false; } auto apply_prop = target->GetProperties()->GetRegistrar()->FindProperty(prop_name); if (!apply_prop) { return false; } return mapper->ApplyEntityPropertyText(target, apply_prop, old_value); }, [entity_id, prop_name, new_value](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { auto target = mapper->FindEntityById(*active_map, entity_id); if (!target) { return false; } auto apply_prop = target->GetProperties()->GetRegistrar()->FindProperty(prop_name); if (!apply_prop) { return false; } return mapper->ApplyEntityPropertyText(target, apply_prop, new_value); }}); } } } void MapperEngine::SelectInspectorPropertyLine(int32_t line) { FO_STACK_TRACE_ENTRY(); constexpr int32_t start_line = 3; InspectorSelectedLine = line; InspectorSelectedLine = std::max(InspectorSelectedLine, 0); InspectorSelectedLineInitialValue = InspectorSelectedLineValue = ""; if (auto entity = GetInspectorEntity()) { if (InspectorSelectedLine - start_line >= numeric_cast<int32_t>(ShowProps.size())) { InspectorSelectedLine = numeric_cast<int32_t>(ShowProps.size()) + start_line - 1; } if (InspectorSelectedLine >= start_line && InspectorSelectedLine - start_line < numeric_cast<int32_t>(ShowProps.size()) && ShowProps[InspectorSelectedLine - start_line]) { auto selected_prop = ShowProps[InspectorSelectedLine - start_line]; FO_VERIFY_AND_THROW(selected_prop, "Selected property is null"); InspectorSelectedLineInitialValue = InspectorSelectedLineValue = entity->GetProperties()->SavePropertyToText(selected_prop); } } } void MapperEngine::ResetInspectorPropertyEditState() { FO_STACK_TRACE_ENTRY(); InspectorEditLine = -1; InspectorPendingFocusLine = -1; InspectorPendingFocusArrayIndex = -1; InspectorPendingCaretResetLine = -1; InspectorPendingCaretResetArrayIndex = -1; InspectorPendingCaretResetFrames = 0; InspectorLastEditCellRectValid = false; } auto MapperEngine::CancelInspectorPropertyEdit() -> bool { FO_STACK_TRACE_ENTRY(); if (InspectorEditLine == -1) { return false; } InspectorSelectedLineValue = InspectorSelectedLineInitialValue; InspectorEditBuf = InspectorSelectedLineValue; if (InspectorEditBuf.capacity() < 4096) { InspectorEditBuf.reserve(4096); } ResetInspectorPropertyEditState(); return true; } auto MapperEngine::GetInspectorEntity() -> nptr<ClientEntity> { FO_STACK_TRACE_ENTRY(); nptr<ClientEntity> entity = nullptr; if (ActivePanelMode == INT_MODE_INCONT && InContItem) { entity = InContItem; } else if (!SelectedEntities.empty()) { entity = SelectedEntities.front(); } if (entity == InspectorEntity) { return entity; } InspectorEntity = entity; ShowProps.clear(); if (entity) { vector<int32_t> prop_indices; OnInspectorProperties.Fire(entity, prop_indices); for (auto prop_index : prop_indices) { ShowProps.emplace_back(prop_index != -1 ? entity->GetProperties()->GetRegistrar()->GetPropertyByIndex(prop_index) : nullptr); } } SelectInspectorPropertyLine(InspectorSelectedLine); return entity; } void MapperEngine::HandleLeftMouseDown() { FO_STACK_TRACE_ENTRY(); MouseHoldMode = INT_NONE; if (HandleMapLeftMouseDown()) { return; } } auto MapperEngine::HandleMapLeftMouseDown() -> bool { FO_STACK_TRACE_ENTRY(); InContItem.reset(); if (!_curMap) { return true; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!cur_map->GetHexAtScreen(MousePos, SelectHex1, nullptr)) { return true; } SelectHex2 = SelectHex1; SelectPos = MousePos; if (CurMode == CUR_MODE_PLACE_OBJECT) { return true; } auto entity = cur_map->GetEntityAtScreen(MousePos, 0, true); auto clicked_entity = entity.first; bool clicked_selected = clicked_entity && SelectedEntitiesSet.contains(clicked_entity); if (clicked_selected) { MouseHoldMode = INT_MOVE_SELECTION; SetCurMode(CUR_MODE_MOVE_SELECTION); return true; } if (GetApp()->Input.IsShiftDown()) { for (size_t i = 0; i < SelectedEntities.size(); i++) { if (auto cr = SelectedEntities[i].dyn_cast<CritterHexView>()) { auto hex = cr->GetHex(); if (auto find_path = cur_map->FindPath(nullptr, hex, SelectHex1, -1)) { for (auto dir : find_path->DirSteps) { if (GeometryHelper::MoveHexByDir(hex, dir, cur_map->GetSize())) { cur_map->MoveCritter(cr, hex, true); } else { break; } } } break; } } } else if (!GetApp()->Input.IsCtrlDown()) { SelectClear(); } MouseHoldMode = INT_SELECT; return true; } void MapperEngine::HandleLeftMouseUp() { FO_STACK_TRACE_ENTRY(); if (CurMode == CUR_MODE_PLACE_OBJECT) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (cur_map->GetHexAtScreen(MousePos, SelectHex2, nullptr)) { if (IsItemMode() && ActiveItemProtos && !ActiveItemProtos->empty()) { CreateItem((*ActiveItemProtos)[GetActiveProtoIndex()]->GetProtoId(), SelectHex2, nullptr); } else if (IsCritMode() && ActiveCritterProtos && !ActiveCritterProtos->empty()) { CreateCritter((*ActiveCritterProtos)[GetActiveProtoIndex()]->GetProtoId(), SelectHex2); } SetCurMode(CUR_MODE_DEFAULT); } MouseHoldMode = INT_NONE; return; } if (MouseHoldMode == INT_MOVE_SELECTION) { CommitPendingSelectionMove(); MouseHoldMode = INT_NONE; SetCurMode(CUR_MODE_DEFAULT); return; } if (MouseHoldMode == INT_SELECT) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (cur_map->GetHexAtScreen(MousePos, SelectHex2, nullptr)) { if (SelectHex1 != SelectHex2) { ClearMapperTrackOverlay(); vector<mpos> hexes; if (SelectAxialGrid) { hexes = GeometryHelper::GetAxialHexes(SelectHex1, SelectHex2, cur_map->GetSize()); } else { msize map_size = cur_map->GetSize(); int32_t fx = std::min(SelectHex1.x, SelectHex2.x); int32_t tx = std::max(SelectHex1.x, SelectHex2.x); int32_t fy = std::min(SelectHex1.y, SelectHex2.y); int32_t ty = std::max(SelectHex1.y, SelectHex2.y); for (int32_t i = fx; i <= tx; i++) { for (int32_t j = fy; j <= ty; j++) { hexes.emplace_back(map_size.from_raw_pos(i, j)); } } } auto check_item_to_add = [&](ptr<const ItemHexView> item) -> bool { if (cur_map->IsIgnorePid(item->GetProtoId())) { return false; } if (item->GetIsTile() && !item->GetIsRoofTile() && SelectTilesEnabled && Settings->ShowTile) { return true; } else if (item->GetIsTile() && item->GetIsRoofTile() && SelectRoofTilesEnabled && Settings->ShowRoof) { return true; } else if (!item->GetIsTile() && !item->GetIsScenery() && !item->GetIsWall() && SelectItemsEnabled && Settings->ShowItem) { return true; } else if (!item->GetIsTile() && item->GetIsScenery() && SelectSceneryEnabled && Settings->ShowScen) { return true; } else if (!item->GetIsTile() && item->GetIsWall() && SelectWallsEnabled && Settings->ShowWall) { return true; } else if (Settings->ShowFast && cur_map->IsFastPid(item->GetProtoId())) { return true; } else { return false; } }; for (auto hex : hexes) { for (ptr<ItemHexView> hex_item : copy_hold_ref(cur_map->GetItemsOnHex(hex))) { if (check_item_to_add(hex_item)) { SelectAdd(hex_item, hex, true); } } for (ptr<CritterHexView> hex_cr : copy_hold_ref(cur_map->GetCrittersOnHex(hex, CritterFindType::Any))) { if (SelectCrittersEnabled && Settings->ShowCrit) { SelectAdd(hex_cr, hex); } } } cur_map->DefferedRefreshItems(); } else { auto entity = cur_map->GetEntityAtScreen(MousePos, 0, true); if (auto item = entity.first.dyn_cast<ItemHexView>()) { if (!item->GetIsTile()) { SelectAdd(item, entity.second->GetHex()); } } else if (auto cr = entity.first.dyn_cast<CritterHexView>()) { SelectAdd(cr, entity.second->GetHex()); } } if (!SelectedEntities.empty() && !GetEntityInnerItems(SelectedEntities.front()).empty()) { SetActivePanelMode(INT_MODE_INCONT); } cur_map->RebuildMap(); } } MouseHoldMode = INT_NONE; SetCurMode(CUR_MODE_DEFAULT); } void MapperEngine::HandleSelectionMouseDrag() { FO_STACK_TRACE_ENTRY(); if (IsImGuiMouseCaptured() || (MouseHoldMode != INT_SELECT && MouseHoldMode != INT_MOVE_SELECTION)) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); bool had_track_overlay = !MapperTrackOverlayHexes.empty(); ClearMapperTrackOverlay(); if (!cur_map->GetHexAtScreen(MousePos, SelectHex2, nullptr)) { if (!SelectHex2.is_zero() || had_track_overlay) { cur_map->RebuildMap(); SelectHex2 = {}; } return; } if (MouseHoldMode == INT_SELECT) { if (SelectHex1 != SelectHex2) { if (SelectAxialGrid) { for (auto hex : GeometryHelper::GetAxialHexes(SelectHex1, SelectHex2, cur_map->GetSize())) { AddMapperTrackOverlayHex(hex, 1); } } else { msize map_size = cur_map->GetSize(); int32_t fx = std::min(SelectHex1.x, SelectHex2.x); int32_t tx = std::max(SelectHex1.x, SelectHex2.x); int32_t fy = std::min(SelectHex1.y, SelectHex2.y); int32_t ty = std::max(SelectHex1.y, SelectHex2.y); for (int32_t i = fx; i <= tx; i++) { for (int32_t j = fy; j <= ty; j++) { AddMapperTrackOverlayHex(map_size.from_raw_pos(i, j), 1); } } } cur_map->RebuildMap(); } else if (had_track_overlay) { cur_map->RebuildMap(); } } else if (MouseHoldMode == INT_MOVE_SELECTION) { int32_t offs_hx = numeric_cast<int32_t>(SelectHex2.x) - numeric_cast<int32_t>(SelectHex1.x); int32_t offs_hy = numeric_cast<int32_t>(SelectHex2.y) - numeric_cast<int32_t>(SelectHex1.y); int32_t offs_x = MousePos.x - SelectPos.x; int32_t offs_y = MousePos.y - SelectPos.y; if (SelectMove(!GetApp()->Input.IsShiftDown(), offs_hx, offs_hy, offs_x, offs_y)) { SelectHex1 = cur_map->GetSize().from_raw_pos(SelectHex1.x + offs_hx, SelectHex1.y + offs_hy); SelectPos.x += offs_x; SelectPos.y += offs_y; cur_map->RebuildMap(); } } } void MapperEngine::SetMapperHexOverlayVisible(bool visible) { FO_STACK_TRACE_ENTRY(); if (MapperHexOverlayVisible == visible) { return; } MapperHexOverlayVisible = visible; if (_curMap != nullptr) { _curMap->RebuildMap(); } } void MapperEngine::ToggleMapperHexOverlay() { FO_STACK_TRACE_ENTRY(); SetMapperHexOverlayVisible(!MapperHexOverlayVisible); } void MapperEngine::ClearMapperTrackOverlay() { FO_STACK_TRACE_ENTRY(); MapperTrackOverlayHexes.clear(); MapperTrackOverlayKinds.clear(); } void MapperEngine::AddMapperTrackOverlayHex(mpos hex, int32_t kind) { FO_STACK_TRACE_ENTRY(); if (_curMap != nullptr && !_curMap->GetSize().is_valid_pos(hex)) { return; } int32_t normalized_kind = kind == 2 ? 2 : 1; MapperTrackOverlayHexes.emplace_back(hex); MapperTrackOverlayKinds.emplace_back(normalized_kind); } void MapperEngine::MarkBlockedHexes() { FO_STACK_TRACE_ENTRY(); ClearMapperTrackOverlay(); if (!_curMap) { return; } msize map_size = _curMap->GetSize(); for (int32_t hx = 0; hx < map_size.width; hx++) { for (int32_t hy = 0; hy < map_size.height; hy++) { mpos hex = map_size.from_raw_pos(hx, hy); const MapView::Field& field = _curMap->GetField(hex); int32_t kind = 0; if (field.MoveBlocked) { kind = 2; } if (field.ShootBlocked) { kind = 1; } if (kind != 0) { AddMapperTrackOverlayHex(hex, kind); } } } _curMap->RebuildMap(); } auto MapperEngine::GetActiveSubTab() -> nptr<SubTab> { FO_STACK_TRACE_ENTRY(); if (ActivePanelMode < 0 || ActivePanelMode >= TAB_COUNT) { return nullptr; } return ActiveSubTabs[ActivePanelMode]; } auto MapperEngine::GetActiveSubTab() const -> nptr<const SubTab> { FO_STACK_TRACE_ENTRY(); if (ActivePanelMode < 0 || ActivePanelMode >= TAB_COUNT) { return nullptr; } return ActiveSubTabs[ActivePanelMode]; } auto MapperEngine::GetActiveProtoIndex() const -> int32_t { FO_STACK_TRACE_ENTRY(); if (auto active_subtab = GetActiveSubTab()) { return active_subtab->Index; } if (ActivePanelMode >= 0 && ActivePanelMode < INT_MODE_COUNT) { return TabIndex[ActivePanelMode]; } return 0; } void MapperEngine::SetActiveProtoIndex(int32_t index) { FO_STACK_TRACE_ENTRY(); if (auto active_subtab = GetActiveSubTab()) { active_subtab->Index = index; } if (ActivePanelMode >= 0 && ActivePanelMode < INT_MODE_COUNT) { TabIndex[ActivePanelMode] = index; } } void MapperEngine::RefreshActiveProtoLists() { FO_STACK_TRACE_ENTRY(); // Select protos and scroll ActiveItemProtos = nullptr; ActiveProtoScroll = nullptr; ActiveCritterProtos = nullptr; InContItem.reset(); if (auto stab = GetActiveSubTab()) { if (!stab->CritterProtos.empty()) { ActiveCritterProtos = &stab->CritterProtos; } else { ActiveItemProtos = &stab->ItemProtos; } ActiveProtoScroll = &stab->Scroll; } if (_curMap) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); // Update fast pids cur_map->ClearFastPids(); for (const auto& fast_proto : ActiveSubTabs[INT_MODE_FAST]->ItemProtos) { cur_map->AddFastPid(fast_proto->GetProtoId()); } // Update ignore pids cur_map->ClearIgnorePids(); for (const auto& ignore_proto : ActiveSubTabs[INT_MODE_IGNORE]->ItemProtos) { cur_map->AddIgnorePid(ignore_proto->GetProtoId()); } // Refresh map cur_map->RebuildMap(); } } void MapperEngine::SetActivePanelMode(int32_t mode) { FO_STACK_TRACE_ENTRY(); ActivePanelMode = mode; MouseHoldMode = INT_NONE; RefreshActiveProtoLists(); } void MapperEngine::MoveEntity(ptr<ClientEntity> entity, mpos hex) { FO_STACK_TRACE_ENTRY(); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!cur_map->GetSize().is_valid_pos(hex)) { return; } mpos old_hex; if (auto cr = entity.dyn_cast<CritterHexView>()) { old_hex = cr->GetHex(); } else if (auto item = entity.dyn_cast<ItemHexView>()) { old_hex = item->GetHex(); } if (old_hex == hex) { return; } ident_t entity_id = entity->GetId(); SelectClear(); if (auto cr = entity.dyn_cast<CritterHexView>()) { cur_map->MoveCritter(cr, hex, false); } else if (auto item = entity.dyn_cast<ItemHexView>()) { cur_map->MoveItem(item, hex); } SetMapDirty(GetCurMap()); PushUndoOp(GetCurMap(), UndoOp {"Move entity", [entity_id, old_hex](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { auto target = mapper->FindEntityById(*active_map, entity_id); if (!target) { return false; } mapper->MoveEntity(target, old_hex); return true; }, [entity_id, hex](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { auto target = mapper->FindEntityById(*active_map, entity_id); if (!target) { return false; } mapper->MoveEntity(target, hex); return true; }}); } void MapperEngine::DeleteEntity(ptr<ClientEntity> entity) { FO_STACK_TRACE_ENTRY(); auto cur_map = GetCurMap(); EntityBuf entity_buf; CaptureEntityBuf(entity_buf, entity); auto item_ownership = ItemOwnership::MapHex; ident_t owner_id {}; if (auto item = entity.dyn_cast<ItemView>()) { item_ownership = item->GetOwnership(); if (item_ownership == ItemOwnership::CritterInventory) { owner_id = item->GetCritterId(); } else if (item_ownership == ItemOwnership::ItemContainer) { owner_id = item->GetContainerId(); } } // Deleting an entity that was never selected is ordinary editing, so both selection stores are cleared // tolerantly. Anything that clears the selection first - moving an entity, an undo step, a redo step - // would otherwise make the very next delete fail with a lookup error. SelectedEntitiesSet.erase(entity); (void)vec_safe_remove_unique_value(SelectedEntities, entity.hold_ref()); if (auto cr = entity.dyn_cast<CritterHexView>()) { cur_map->DestroyCritter(cr); } else if (auto item = entity.dyn_cast<ItemHexView>()) { cur_map->DestroyItem(item); } else if (auto inner_item = entity.dyn_cast<ItemView>()) { if (inner_item->GetOwnership() == ItemOwnership::CritterInventory) { if (auto owner = cur_map->GetCritter(inner_item->GetCritterId())) { owner->DeleteInvItem(inner_item); } } else if (inner_item->GetOwnership() == ItemOwnership::ItemContainer) { if (auto owner = cur_map->GetItem(inner_item->GetContainerId())) { owner->DestroyInnerItem(inner_item); } } } SetMapDirty(GetCurMap()); PushUndoOp(cur_map, UndoOp {"Delete entity", [entity_buf, item_ownership, owner_id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { nptr<ClientEntity> owner; if (item_ownership == ItemOwnership::CritterInventory || item_ownership == ItemOwnership::ItemContainer) { owner = mapper->FindEntityById(*active_map, owner_id); if (!owner) { return false; } } return !!mapper->RestoreEntityBuf(entity_buf, owner); }, [entity_id = entity_buf.Id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { if (auto target = mapper->FindEntityById(*active_map, entity_id)) { mapper->DeleteEntity(target); return true; } return false; }}); } void MapperEngine::SetSelectionContour(ptr<ClientEntity> entity, ucolor color) const { FO_STACK_TRACE_ENTRY(); // Selection outline goes through the same script contour pipeline as the client (ContourPipeline.fos): // write the entity's Contour property; the script's property-setter caches it and OnRenderMap_AfterSprites // draws it. Mapper-only callers, but the Contour property exists on every Item/Critter. auto prop = entity->GetProperties()->GetRegistrar()->FindProperty("Contour"); if (prop) { entity->GetPropertiesForEdit()->SetValue<ucolor>(prop, color); } } void MapperEngine::SelectAdd(ptr<ClientEntity> entity, optional<mpos> hex, bool skip_refresh) { FO_STACK_TRACE_ENTRY(); if (entity->IsDestroyed()) { return; } if (SelectedEntitiesSet.contains(entity)) { return; } auto corrected_entity = entity; auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); // Break from merged mesh if (!SelectEntireEntity && hex.has_value()) { if (auto item = corrected_entity.dyn_cast<ItemHexView>()) { auto broken_item = TryBreakItemFromMultihexMesh(cur_map, item, hex.value()); if (!broken_item) { return; } corrected_entity = broken_item; } } vec_add_unique_value(SelectedEntities, corrected_entity.hold_ref()); SelectedEntitiesSet.emplace(corrected_entity); InspectorVisible = true; // Make transparent + outline the selection (contour via the script pipeline) if (auto hex_view = corrected_entity.dyn_cast<HexView>()) { hex_view->SetTargetAlpha(SelectAlpha); SetSelectionContour(corrected_entity, SelectContourColor); } if (!skip_refresh) { cur_map->DefferedRefreshItems(); } } void MapperEngine::SelectAll() { FO_STACK_TRACE_ENTRY(); SelectClear(); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); span<refcount_ptr<ItemHexView>> items = cur_map->GetItems(); for (size_t i = 0; i < items.size(); i++) { auto item = items[i].as_ptr(); if (cur_map->IsIgnorePid(item->GetProtoId())) { continue; } if ((!item->GetIsScenery() && !item->GetIsWall() && SelectItemsEnabled && Settings->ShowItem) || // (item->GetIsScenery() && SelectSceneryEnabled && Settings->ShowScen) || // (item->GetIsWall() && SelectWallsEnabled && Settings->ShowWall) || // (item->GetIsTile() && !item->GetIsRoofTile() && SelectTilesEnabled && Settings->ShowTile) || // (item->GetIsTile() && item->GetIsRoofTile() && SelectRoofTilesEnabled && Settings->ShowRoof)) { SelectAdd(item); } } if (SelectCrittersEnabled && Settings->ShowCrit) { span<refcount_ptr<CritterHexView>> critters = cur_map->GetCritters(); for (size_t i = 0; i < critters.size(); i++) { SelectAdd(critters[i]); } } cur_map->RebuildMap(); } void MapperEngine::SelectRemove(ptr<ClientEntity> entity, bool skip_refresh) { FO_STACK_TRACE_ENTRY(); if (SelectedEntitiesSet.erase(entity) == 0) { return; } auto entity_holder = entity.hold_ref(); vec_remove_unique_value(SelectedEntities, entity_holder); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); // Delete intersected tiles if (!SelectEntireEntity && !entity->IsDestroyed()) { if (auto tile = entity.dyn_cast<ItemHexView>()) { if (tile->GetIsTile()) { vector<ptr<ItemHexView>> same_siblings; for (ptr<ItemHexView> item : cur_map->GetItemsOnHex(tile->GetHex())) { if (item != tile && item->GetIsTile() == tile->GetIsTile() && item->GetIsRoofTile() == tile->GetIsRoofTile() && // item->GetTileLayer() == tile->GetTileLayer() && !SelectedEntitiesSet.contains(item)) { same_siblings.emplace_back(item); } } for (ptr<ItemHexView> same_sibling : copy_hold_ref(same_siblings)) { if (!same_sibling->IsDestroyed()) { auto broken_item = TryBreakItemFromMultihexMesh(cur_map, same_sibling, tile->GetHex()); if (broken_item) { cur_map->DestroyItem(broken_item); } } } } } } // Restore alpha + drop the selection outline if (!entity->IsDestroyed()) { if (auto hex_view = entity.dyn_cast<HexView>()) { hex_view->RestoreAlpha(); SetSelectionContour(entity, ucolor::clear); } } // Merge multihex items if (!entity->IsDestroyed()) { if (auto item = entity.dyn_cast<ItemHexView>()) { if (item->GetMultihexGeneration() != MultihexGenerationType::None) { CoalesceItemMultihexMesh(cur_map, item, true); } } } if (!skip_refresh) { cur_map->DefferedRefreshItems(); } } void MapperEngine::SelectClear() { FO_STACK_TRACE_ENTRY(); while (!SelectedEntities.empty()) { SelectRemove(SelectedEntities.back(), true); } InContItem.reset(); InspectorVisible = false; InspectorEntity = nullptr; ShowProps.clear(); InspectorSelectedLine = 0; InspectorSelectedLineInitialValue.clear(); InspectorSelectedLineValue.clear(); InspectorEditBuf.clear(); ResetInspectorPropertyEditState(); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); cur_map->DefferedRefreshItems(); } auto MapperEngine::SelectMove(bool hex_move, int32_t& offs_hx, int32_t& offs_hy, int32_t& offs_x, int32_t& offs_y) -> bool { FO_STACK_TRACE_ENTRY(); if (!hex_move && offs_x == 0 && offs_y == 0) { return false; } if (hex_move && offs_hx == 0 && offs_hy == 0) { return false; } // Tile step bool have_tiles = false; for (size_t i = 0; i != SelectedEntities.size(); i++) { auto item = SelectedEntities[i].dyn_cast<ItemHexView>(); if (!item) { continue; } if (item->GetIsTile()) { have_tiles = true; break; } } if (hex_move && have_tiles) { if (std::abs(offs_hx) < Settings->MapTileStep && std::abs(offs_hy) < Settings->MapTileStep) { return false; } offs_hx -= offs_hx % Settings->MapTileStep; offs_hy -= offs_hy % Settings->MapTileStep; } // Setup hex moving switcher int32_t switcher = 0; if (!SelectedEntities.empty()) { if (auto cr = SelectedEntities.front().dyn_cast<CritterHexView>()) { switcher = std::abs(cr->GetHex().x % 2); } else if (auto item = SelectedEntities.front().dyn_cast<ItemHexView>()) { switcher = std::abs(item->GetHex().x % 2); } } auto move_hex = [&](ipos32& raw_hex) { if constexpr (GameSettings::HEXAGONAL_GEOMETRY) { int32_t sw = switcher; for (int32_t k = 0; k < std::abs(offs_hx); k++, sw++) { GeometryHelper::MoveHexByDirUnsafe(raw_hex, offs_hx > 0 ? ((sw % 2) != 0 ? hdir::West : hdir::SouthWest) : ((sw % 2) != 0 ? hdir::NorthEast : hdir::East)); } for (int32_t k = 0; k < std::abs(offs_hy); k++) { GeometryHelper::MoveHexByDirUnsafe(raw_hex, offs_hy > 0 ? hdir::SouthEast : hdir::NorthWest); } } else { raw_hex.x += offs_hx; raw_hex.y += offs_hy; } }; auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!hex_move) { float32_t& small_ox = SelectionSmallOffsetX; float32_t& small_oy = SelectionSmallOffsetY; float32_t ox = numeric_cast<float32_t>(offs_x) / cur_map->GetSpritesZoom() + small_ox; float32_t oy = numeric_cast<float32_t>(offs_y) / cur_map->GetSpritesZoom() + small_oy; if (offs_x != 0 && std::fabs(ox) < 1.0f) { small_ox = ox; } else { small_ox = 0.0f; } if (offs_y != 0 && std::fabs(oy) < 1.0f) { small_oy = oy; } else { small_oy = 0.0f; } offs_x = iround<int32_t>(ox); offs_y = iround<int32_t>(oy); } else { for (size_t i = 0; i < SelectedEntities.size(); i++) { auto entity = SelectedEntities[i].as_ptr(); ipos32 raw_hex; if (auto cr = entity.dyn_cast<CritterHexView>()) { raw_hex = ipos32 {cr->GetHex().x, cr->GetHex().y}; } else if (auto item = entity.dyn_cast<ItemHexView>()) { raw_hex = ipos32 {item->GetHex().x, item->GetHex().y}; } move_hex(raw_hex); if (!cur_map->GetSize().is_valid_pos(raw_hex)) { return false; } if (auto item = entity.dyn_cast<ItemHexView>()) { if (item->HasMultihexEntries()) { auto multihex_entries = item->GetMultihexEntries(); FO_VERIFY_AND_THROW(multihex_entries, "Multihex entries collection is null"); for (auto hex2 : *multihex_entries) { ipos32 raw_hex2 = ipos32 {hex2.x, hex2.y}; move_hex(raw_hex2); if (!cur_map->GetSize().is_valid_pos(raw_hex2)) { return false; } } } } } } vector<MoveCommandEntry> move_entries; for (size_t i = 0; i < SelectedEntities.size(); i++) { auto entity = SelectedEntities[i].as_ptr(); if (!hex_move) { auto item = entity.dyn_cast<ItemHexView>(); if (!item) { continue; } int32_t ox = item->GetOffset().x + offs_x; int32_t oy = item->GetOffset().y + offs_y; if (GetApp()->Input.IsAltDown()) { ox = oy = 0; } MoveCommandEntry entry; entry.EntityId = item->GetId(); entry.HasOffset = true; entry.OldOffset = item->GetOffset(); entry.NewOffset = {numeric_cast<int16_t>(ox), numeric_cast<int16_t>(oy)}; if (entry.OldOffset == entry.NewOffset) { continue; } move_entries.emplace_back(std::move(entry)); item->SetOffset({numeric_cast<int16_t>(ox), numeric_cast<int16_t>(oy)}); item->RefreshAnim(); } else { ipos32 raw_hex; if (auto cr = entity.dyn_cast<CritterHexView>()) { raw_hex = ipos32 {cr->GetHex().x, cr->GetHex().y}; } else if (auto item = entity.dyn_cast<ItemHexView>()) { raw_hex = ipos32 {item->GetHex().x, item->GetHex().y}; } move_hex(raw_hex); mpos hex = cur_map->GetSize().clamp_pos(raw_hex); if (auto item = entity.dyn_cast<ItemHexView>()) { MoveCommandEntry entry; entry.EntityId = item->GetId(); entry.HasHex = true; entry.OldHex = item->GetHex(); entry.NewHex = hex; if (item->IsNonEmptyMultihexMesh()) { entry.OldMultihexMesh = item->GetMultihexMesh(); auto multihex_mesh = entry.OldMultihexMesh; std::ranges::for_each(multihex_mesh, [&](mpos& hex2) { ipos32 raw_hex2 = ipos32(hex2); move_hex(raw_hex2); hex2 = cur_map->GetSize().clamp_pos(raw_hex2); }); entry.NewMultihexMesh = multihex_mesh; item->SetMultihexMesh(multihex_mesh); } if (entry.OldHex == entry.NewHex && entry.OldMultihexMesh == entry.NewMultihexMesh) { continue; } move_entries.emplace_back(std::move(entry)); cur_map->MoveItem(item, hex); } else if (auto cr = entity.dyn_cast<CritterHexView>()) { MoveCommandEntry entry; entry.EntityId = cr->GetId(); entry.HasHex = true; entry.OldHex = cr->GetHex(); entry.NewHex = hex; if (entry.OldHex == entry.NewHex) { continue; } move_entries.emplace_back(std::move(entry)); cur_map->MoveCritter(cr, hex, false); } } } if (move_entries.empty()) { return false; } SetMapDirty(GetCurMap()); cur_map->DefferedRefreshItems(); for (const auto& entry : move_entries) { auto it = std::ranges::find_if(PendingSelectionMoveEntries, [&](const MoveCommandEntry& pending_entry) { return pending_entry.EntityId == entry.EntityId; }); if (it == PendingSelectionMoveEntries.end()) { PendingSelectionMoveEntries.emplace_back(entry); continue; } if (entry.HasOffset) { it->HasOffset = true; it->NewOffset = entry.NewOffset; } if (entry.HasHex) { it->HasHex = true; it->NewHex = entry.NewHex; it->NewMultihexMesh = entry.NewMultihexMesh; } } return true; } void MapperEngine::SelectDelete() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!UndoRedoInProgress && SelectedEntities.size() > 1) { struct DeleteCommandEntry { EntityBuf Entity {}; ItemOwnership Ownership {ItemOwnership::MapHex}; ident_t OwnerId {}; }; vector<DeleteCommandEntry> delete_entries; delete_entries.reserve(SelectedEntities.size()); for (ptr<ClientEntity> entity : copy_hold_ref(SelectedEntities)) { DeleteCommandEntry entry; CaptureEntityBuf(entry.Entity, entity); if (auto item = entity.dyn_cast<ItemView>()) { entry.Ownership = item->GetOwnership(); if (entry.Ownership == ItemOwnership::CritterInventory) { entry.OwnerId = item->GetCritterId(); } else if (entry.Ownership == ItemOwnership::ItemContainer) { entry.OwnerId = item->GetContainerId(); } } delete_entries.emplace_back(std::move(entry)); } UndoRedoInProgress = true; auto progress_guard = scope_fail([this]() noexcept { UndoRedoInProgress = false; }); for (ptr<ClientEntity> entity : copy_hold_ref(SelectedEntities)) { DeleteEntity(entity); } UndoRedoInProgress = false; SelectClear(); cur_map->RebuildMap(); MouseHoldMode = INT_NONE; SetCurMode(CUR_MODE_DEFAULT); PushUndoOp(GetCurMap(), UndoOp {"Delete selection", [delete_entries](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { for (const auto& entry : delete_entries) { nptr<ClientEntity> owner; if (entry.Ownership == ItemOwnership::CritterInventory || entry.Ownership == ItemOwnership::ItemContainer) { owner = mapper->FindEntityById(*active_map, entry.OwnerId); if (!owner) { return false; } } if (!mapper->RestoreEntityBuf(entry.Entity, owner)) { return false; } } mapper->SetMapDirty(*active_map); return true; }, [delete_entries](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { for (auto it = delete_entries.rbegin(); it != delete_entries.rend(); ++it) { if (auto target = mapper->FindEntityById(*active_map, it->Entity.Id)) { mapper->DeleteEntity(target); } } return true; }}); return; } for (ptr<ClientEntity> entity : copy_hold_ref(SelectedEntities)) { DeleteEntity(entity); } SelectClear(); cur_map->RebuildMap(); MouseHoldMode = INT_NONE; SetCurMode(CUR_MODE_DEFAULT); } auto MapperEngine::CreateCritter(hstring pid, mpos hex) -> ptr<CritterView> { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(_curMap, "Mapper has no current map"); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!cur_map->GetSize().is_valid_pos(hex)) { throw GenericException("Invalid hex for critter spawn", pid, hex.x, hex.y); } if (cur_map->GetNonDeadCritter(hex)) { throw GenericException("Hex is already occupied by a non-dead critter", pid, hex.x, hex.y); } SelectClear(); auto cr = cur_map->AddMapperCritter(pid, hex, CritterDir, nullptr); SelectAdd(cr, hex); SetMapDirty(GetCurMap()); cur_map->RebuildMap(); SetCurMode(CUR_MODE_DEFAULT); EntityBuf entity_buf; CaptureEntityBuf(entity_buf, cr); PushUndoOp(GetCurMap(), UndoOp {"Create critter", [entity_id = entity_buf.Id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { if (auto target = mapper->FindEntityById(*active_map, entity_id)) { mapper->DeleteEntity(target); return true; } return false; }, [entity_buf](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { ignore_unused(active_map); return !!mapper->RestoreEntityBuf(entity_buf); }}); return cr; } auto MapperEngine::CreateItem(hstring pid, mpos hex, nptr<Entity> owner) -> ptr<ItemView> { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(_curMap, "Mapper has no current map"); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); auto proto = GetProtoItem(pid); if (!proto) { throw GenericException("Invalid item proto", pid); } mpos corrected_hex = hex; if (proto->GetIsTile()) { corrected_hex = cur_map->GetSize().from_raw_pos(corrected_hex.x - corrected_hex.x % Settings->MapTileStep, corrected_hex.y - corrected_hex.y % Settings->MapTileStep); } if (!owner && (!cur_map->GetSize().is_valid_pos(corrected_hex))) { throw GenericException("Invalid hex for item spawn", pid, hex.x, hex.y); } if (!owner) { SelectClear(); } nptr<ItemView> created_item; if (owner) { if (auto cr = owner.dyn_cast<CritterHexView>()) { created_item = cr->AddMapperInvItem(cr->GetMap()->GenTempEntityId(), proto, CritterItemSlot::Inventory, {}); } if (auto cont = owner.dyn_cast<ItemHexView>()) { created_item = cont->AddMapperInnerItem(cont->GetMap()->GenTempEntityId(), proto, {}, nullptr); } } else if (proto->GetIsTile()) { created_item = cur_map->AddMapperTile(proto->GetProtoId(), corrected_hex, numeric_cast<uint8_t>(TileLayer), PreviewRoofTiles); } else { created_item = cur_map->AddMapperItem(proto->GetProtoId(), corrected_hex, nullptr); } if (!created_item) { throw GenericException("Failed to create item", pid); } if (!owner) { SelectAdd(created_item, corrected_hex); SetCurMode(CUR_MODE_DEFAULT); } else { SetActivePanelMode(INT_MODE_INCONT); InContItem = created_item.hold_ref(); } SetMapDirty(GetCurMap()); { EntityBuf entity_buf; CaptureEntityBuf(entity_buf, created_item); auto item_ownership = created_item->GetOwnership(); ident_t owner_id {}; if (item_ownership == ItemOwnership::CritterInventory) { owner_id = created_item->GetCritterId(); } else if (item_ownership == ItemOwnership::ItemContainer) { owner_id = created_item->GetContainerId(); } PushUndoOp(GetCurMap(), UndoOp {"Create item", [entity_id = entity_buf.Id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { if (auto target = mapper->FindEntityById(*active_map, entity_id)) { mapper->DeleteEntity(target); return true; } return false; }, [entity_buf, item_ownership, owner_id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { nptr<ClientEntity> restore_owner; if (item_ownership == ItemOwnership::CritterInventory || item_ownership == ItemOwnership::ItemContainer) { restore_owner = mapper->FindEntityById(*active_map, owner_id); if (!restore_owner) { return false; } } return !!mapper->RestoreEntityBuf(entity_buf, restore_owner); }}); } return created_item; } auto MapperEngine::CloneEntity(ptr<Entity> entity) -> nptr<Entity> { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(_curMap, "Mapper has no current map"); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (auto cr = entity.dyn_cast<CritterHexView>()) { auto cr_clone = cur_map->AddMapperCritter(cr->GetProtoId(), cr->GetHex(), cr->GetDir(), cr->GetProperties()); const_span<refcount_ptr<ItemView>> inv_items = cr->GetInvItems(); for (size_t i = 0; i < inv_items.size(); i++) { auto inv_item = inv_items[i].as_ptr(); auto inv_item_proto = inv_item->GetProto().dyn_cast<const ProtoItem>(); FO_VERIFY_AND_THROW(inv_item_proto, "Inventory item prototype is not an item prototype"); auto inv_item_clone = cr_clone->AddMapperInvItem(cur_map->GenTempEntityId(), inv_item_proto, inv_item->GetCritterSlot(), {}); CloneInnerItems(cur_map, inv_item_clone, inv_item); } SelectAdd(cr_clone); SetMapDirty(GetCurMap()); EntityBuf entity_buf; CaptureEntityBuf(entity_buf, cr_clone); PushUndoOp(GetCurMap(), UndoOp {"Clone entity", [entity_id = entity_buf.Id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { if (auto target = mapper->FindEntityById(*active_map, entity_id)) { mapper->DeleteEntity(target); return true; } return false; }, [entity_buf](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { ignore_unused(active_map); return !!mapper->RestoreEntityBuf(entity_buf); }}); return cr_clone; } if (auto item = entity.dyn_cast<ItemHexView>()) { auto item_clone = cur_map->AddMapperItem(item->GetProtoId(), item->GetHex(), item->GetProperties()); CloneInnerItems(cur_map, item_clone, item); SelectAdd(item_clone); SetMapDirty(GetCurMap()); EntityBuf entity_buf; CaptureEntityBuf(entity_buf, item_clone); PushUndoOp(GetCurMap(), UndoOp {"Clone entity", [entity_id = entity_buf.Id](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { if (auto target = mapper->FindEntityById(*active_map, entity_id)) { mapper->DeleteEntity(target); return true; } return false; }, [entity_buf](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { ignore_unused(active_map); return !!mapper->RestoreEntityBuf(entity_buf); }}); return item_clone; } return nullptr; } void MapperEngine::CloneInnerItems(ptr<MapView> map, ptr<ItemView> to_item, ptr<const ItemView> from_item) { FO_STACK_TRACE_ENTRY(); const_span<refcount_ptr<ItemView>> inner_items = from_item->GetInnerItems(); for (size_t i = 0; i < inner_items.size(); i++) { auto inner_item = inner_items[i].as_ptr(); any_t stack_id = any_t {string(inner_item->GetContainerStack())}; auto inner_item_proto = inner_item->GetProto().dyn_cast<const ProtoItem>(); FO_VERIFY_AND_THROW(inner_item_proto, "Inner item prototype is not an item prototype"); auto inner_item_clone = to_item->AddMapperInnerItem(map->GenTempEntityId(), inner_item_proto, stack_id, from_item->GetProperties()); CloneInnerItems(map, inner_item_clone, inner_item); } } auto MapperEngine::MergeItemsToMultihexMeshes(ptr<MapView> map) -> size_t { FO_STACK_TRACE_ENTRY(); size_t merges = 0; // AnyUnique items merge by (proto, data), independent of position, into the lowest-id group member. Doing // that with the generic per-step loop is O(N^2) (a full-map rescan per merge - the dominant cost of loading // a large tiled map). Coalesce them all in one O(N*groups) grouping pass; SameSibling and AnyUnique never // interact (MultihexGeneration is a proto property), so the result is identical to the interleaved loops. merges += CoalesceAnyUniqueItems(map, false); // SameSibling items merge spatially; the per-step best-by-id selection is path-dependent, so keep the exact // two-phase order (modified items first, then the rest) and the incremental driver. // First merge to modified items for (ptr<ItemHexView> item : copy_hold_ref(map->GetItems())) { if (!item->IsDestroyed() && item->GetMultihexGeneration() == MultihexGenerationType::SameSibling) { small_vector<ptr<const Property>, 2> ignore_props {item->GetPropertyHex(), item->GetPropertyMultihexMesh()}; if (!item->GetProperties()->CompareData(*item->GetProto()->GetProperties(), ignore_props, true)) { merges += CoalesceItemMultihexMesh(map, item, false); } } } // Rest merge clean items for (ptr<ItemHexView> item : copy_hold_ref(map->GetItems())) { if (!item->IsDestroyed() && item->GetMultihexGeneration() == MultihexGenerationType::SameSibling) { merges += CoalesceItemMultihexMesh(map, item, false); } } // Normalize mutihex meshes (sort and move origin to first entry) auto hex_less = [](auto&& hex1, auto&& hex2) { return hex1.y == hex2.y ? hex1.x < hex2.x : hex1.y < hex2.y; }; span<refcount_ptr<ItemHexView>> map_items = map->GetItems(); for (size_t i = 0; i < map_items.size(); i++) { auto item = map_items[i].as_ptr(); if (item->IsNonEmptyMultihexMesh()) { auto multihex_mesh = item->GetMultihexMesh(); std::ranges::sort(multihex_mesh, hex_less); if (hex_less(multihex_mesh.front(), item->GetHex())) { auto hex = multihex_mesh.front(); multihex_mesh.front() = item->GetHex(); std::ranges::sort(multihex_mesh, hex_less); item->SetMultihexMesh(multihex_mesh); map->MoveItem(item, hex); } else { item->SetMultihexMesh(multihex_mesh); } } } map->DefferedRefreshItems(); return merges; } auto MapperEngine::CoalesceAnyUniqueItems(ptr<MapView> map, bool skip_selected) -> size_t { FO_STACK_TRACE_ENTRY(); // O(N*groups) bulk coalescence of every AnyUnique item on the map. AnyUnique merges any same-proto items // that share per-item data (ignoring Hex and MultihexMesh) into a single mesh, regardless of position, // collapsing each (proto, data) group into its lowest-id member - exactly what the original per-step loop // (TryMergeItemToMultihexMesh's AnyUnique branch + the MergeItemsToMultihexMeshes while-loops) converges to, // but without the O(N^2) full-map rescan per merge. The per-group merge sequence is irrelevant to the final // stored mesh because MergeItemsToMultihexMeshes normalizes (sorts + origin-to-smallest) afterward. size_t merges = 0; // One (proto, data) group: its current lowest-id survivor plus the members still to be merged into it. struct UniqueGroup { ptr<ItemHexView> survivor; vector<ptr<ItemHexView>> to_merge; }; // Group representatives are bucketed by proto id so data comparison is only ever done within a single // proto. Within a proto the number of distinct data signatures is small in practice (clean tiles plus a // few authored variants), so the linear "first matching group" assignment stays effectively O(N). unordered_map<hstring, vector<UniqueGroup>> groups_by_proto; for (ptr<ItemHexView> item : copy_hold_ref(map->GetItems())) { if (item->IsDestroyed() || item->GetMultihexGeneration() != MultihexGenerationType::AnyUnique) { continue; } if (skip_selected && SelectedEntitiesSet.contains(item)) { continue; } small_vector<ptr<const Property>, 2> ignore_props {item->GetPropertyHex(), item->GetPropertyMultihexMesh()}; auto& proto_groups = groups_by_proto[item->GetProtoId()]; nptr<UniqueGroup> match; for (auto& group : proto_groups) { if (group.survivor->GetProperties()->CompareData(*item->GetProperties(), ignore_props, true)) { match = make_nptr(&group); break; } } if (match) { if (item->GetId() < match->survivor->GetId()) { // Keep the lowest id as the survivor (the original always merges into the lower id). match->to_merge.emplace_back(match->survivor); match->survivor = item; } else { match->to_merge.emplace_back(item); } } else { proto_groups.emplace_back(UniqueGroup {item, {}}); } } // Merge each group's members into its survivor. Rather than calling MergeItemToMultihexMesh once per // member (each of which rescans the survivor's growing mesh - O(group^2), the second quadratic of a large // tiled map), gather the whole group's hexes once into a dedup set and assign the survivor's mesh a single // time. The resulting hex set is identical; MergeItemsToMultihexMeshes normalizes order afterward. unordered_set<mpos> mesh_hexes; vector<mpos> mesh_vec; // All merged-away sources are destroyed in one bulk pass at the end - destroying them individually is the // third O(N^2) on a large tiled map (each DestroyItem linearly compacts the membership vectors). vector<ptr<ItemHexView>> sources_to_destroy; for (auto& [proto_id, proto_groups] : groups_by_proto) { for (auto& group : proto_groups) { if (group.to_merge.empty()) { continue; } mesh_hexes.clear(); mesh_vec.clear(); auto add_item_hexes = [&](ptr<const ItemHexView> it) { if (it->GetHex() != group.survivor->GetHex() && mesh_hexes.emplace(it->GetHex()).second) { mesh_vec.emplace_back(it->GetHex()); } if (it->IsNonEmptyMultihexMesh()) { for (auto multihex : it->GetMultihexMesh()) { if (multihex != group.survivor->GetHex() && mesh_hexes.emplace(multihex).second) { mesh_vec.emplace_back(multihex); } } } }; add_item_hexes(group.survivor); for (ptr<ItemHexView> source : group.to_merge) { add_item_hexes(source); } group.survivor->SetMultihexMesh(mesh_vec); map->RefreshItem(group.survivor, true); for (ptr<ItemHexView> source : group.to_merge) { sources_to_destroy.emplace_back(source); merges++; } } } map->DestroyItems(sources_to_destroy); return merges; } auto MapperEngine::CoalesceItemMultihexMesh(ptr<MapView> map, ptr<ItemHexView> item, bool skip_selected) -> size_t { FO_STACK_TRACE_ENTRY(); // Incremental driver for the merge loop that used to be `while ((item = TryMergeItemToMultihexMesh(...)) // != nullptr) merges++;`. It produces the exact same sequence of merges (same per-step best-by-id target // and source, same merge direction, same survivor) as repeatedly calling TryMergeItemToMultihexMesh, but // collects the merge candidates ONCE per mesh hex instead of rescanning the whole growing mesh on every // step. That turns each coalesced region from O(N^2) to O(N) (the dominant cost of the map-load merge). // // Only the SameSibling strategy grows a mesh hex by hex (so only it is quadratic); AnyUnique merges by a // full-map scan per step and is left on the original per-step path. if (item->GetMultihexGeneration() != MultihexGenerationType::SameSibling) { size_t merges = 0; nptr<ItemHexView> merge_item = item; while (merge_item) { merge_item = TryMergeItemToMultihexMesh(map, merge_item, skip_selected); if (!merge_item) { break; } merges++; } return merges; } size_t merges = 0; // Candidate sets maintained across the whole loop, mirroring TryMergeItemToMultihexMesh's per-call sets: // an item eligible to be merged INTO (target) and an item eligible to be merged FROM (source), each found // around the survivor's mesh hexes via FindMultihexMeshForItemAroundHex. `scanned` records which hexes // already contributed their neighborhood so each hex is scanned exactly once. unordered_set<ptr<ItemHexView>> target_items; unordered_set<ptr<ItemHexView>> source_items; unordered_set<mpos> scanned; auto scan_hex = [&](mpos hex) { // Mirror find_include_multihex_lines from the original per-step function. multihex_lines is re-read // from the current survivor (proto-derived, so constant within a region) to stay faithful if the // survivor object changes. auto multihex_lines = item->GetMultihexLines(); FindMultihexMeshForItemAroundHex(map, item, hex, true, target_items); FindMultihexMeshForItemAroundHex(map, item, hex, false, source_items); if (!multihex_lines.empty()) { GeometryHelper::ForEachMultihexLines(multihex_lines, hex, map->GetSize(), [&](mpos hex2) { FindMultihexMeshForItemAroundHex(map, item, hex2, true, target_items); FindMultihexMeshForItemAroundHex(map, item, hex2, false, source_items); }); } }; // Scan one hex's neighborhood exactly once. Cheap no-op if the hex was already scanned. This is what keeps // the whole region O(N): every hex contributes its neighborhood to the candidate sets a single time. auto scan_hex_once = [&](mpos hex) { if (scanned.emplace(hex).second) { scan_hex(hex); } }; // Collect a snapshot of every hex an item covers (origin + mesh). Used to fold the hexes a just-merged // party brings into the survivor, scanning ONLY those new hexes instead of re-walking the whole mesh. auto collect_item_hexes = [](ptr<const ItemHexView> it, vector<mpos>& out) { out.clear(); out.emplace_back(it->GetHex()); if (it->IsNonEmptyMultihexMesh()) { auto mesh = it->GetMultihexMesh(); out.insert(out.end(), mesh.begin(), mesh.end()); } }; // Full rebuild of the candidate sets against the current survivor. Used once at the start, and again only // when a merge changes the survivor's DATA (the X->clean transition), because that flips which neighbors // are eligible. Within a constant-data regime the incremental scan below is sufficient. vector<mpos> hex_scratch; auto rebuild = [&]() { target_items.clear(); source_items.clear(); scanned.clear(); collect_item_hexes(item, hex_scratch); for (auto hex : hex_scratch) { scan_hex_once(hex); } }; small_vector<ptr<const Property>, 2> ignore_props {item->GetPropertyHex(), item->GetPropertyMultihexMesh()}; rebuild(); for (;;) { // The survivor never merges into itself; FindMultihexMeshForItemAroundHex would never collect it // (CompareMultihexItemForMerge rejects equal ids), but a prior step may have collected it before it // became the survivor, so drop it explicitly. target_items.erase(item); source_items.erase(item); vector<ptr<ItemHexView>> sorted_target_items = vec_sorted(target_items, [](ptr<ItemHexView> i1, ptr<ItemHexView> i2) { return i1->GetId() < i2->GetId(); }); vector<ptr<ItemHexView>> sorted_source_items = vec_sorted(source_items, [](ptr<ItemHexView> i1, ptr<ItemHexView> i2) { return i2->GetId() < i1->GetId(); }); if (skip_selected) { sorted_target_items = vec_filter(sorted_target_items, [&](ptr<ItemHexView> i) { return !SelectedEntitiesSet.contains(i); }); sorted_source_items = vec_filter(sorted_source_items, [&](ptr<ItemHexView> i) { return !SelectedEntitiesSet.contains(i); }); } nptr<ItemHexView> best_target_item; nptr<ItemHexView> best_source_item; if (!sorted_target_items.empty()) { best_target_item = sorted_target_items.front(); } if (!sorted_source_items.empty()) { best_source_item = sorted_source_items.front(); } nptr<ItemHexView> source_item; nptr<ItemHexView> target_item; if (best_target_item && (!best_source_item || best_target_item->GetId() < item->GetId())) { source_item = item; target_item = best_target_item; } else if (best_source_item) { source_item = best_source_item; target_item = item; } if (!source_item || !target_item) { break; } auto old_survivor = item; // All of the previous survivor's hexes are already scanned; the only hexes new to the merged survivor // come from the OTHER party. Snapshot them before the merge destroys the source. auto incoming_item = target_item == item ? source_item : target_item; collect_item_hexes(incoming_item, hex_scratch); MergeItemToMultihexMesh(map, source_item, target_item); merges++; // The merged-away source is destroyed; it must never leak back as a candidate. target_items.erase(source_item); source_items.erase(source_item); item = target_item; // The survivor's data changes only when it merges INTO a clean target whose (clean) data differs from // the old survivor's data; that flips neighbor eligibility, so rebuild from scratch. This happens at // most once per region (clean data is absorbing), keeping the whole region O(N). bool data_changed = item != old_survivor && !old_survivor->GetProperties()->CompareData(*item->GetProperties(), ignore_props, true); if (data_changed) { rebuild(); } else { for (auto hex : hex_scratch) { scan_hex_once(hex); } } } return merges; } auto MapperEngine::TryMergeItemToMultihexMesh(ptr<MapView> map, ptr<ItemHexView> item, bool skip_selected) -> nptr<ItemHexView> { FO_STACK_TRACE_ENTRY(); if (item->GetMultihexGeneration() == MultihexGenerationType::None) { return nullptr; } nptr<ItemHexView> source_item; nptr<ItemHexView> target_item; // Find mergable item by selected strategy // Always prefer merge from higher ids to lower if (item->GetMultihexGeneration() == MultihexGenerationType::SameSibling) { unordered_set<ptr<ItemHexView>> target_items; unordered_set<ptr<ItemHexView>> source_items; auto multihex_lines = item->GetMultihexLines(); auto find_include_multihex_lines = [&](mpos hex) { FindMultihexMeshForItemAroundHex(map, item, hex, true, target_items); FindMultihexMeshForItemAroundHex(map, item, hex, false, source_items); if (!multihex_lines.empty()) { GeometryHelper::ForEachMultihexLines(multihex_lines, hex, map->GetSize(), [&](mpos hex2) { FindMultihexMeshForItemAroundHex(map, item, hex2, true, target_items); FindMultihexMeshForItemAroundHex(map, item, hex2, false, source_items); }); } }; find_include_multihex_lines(item->GetHex()); if (item->IsNonEmptyMultihexMesh()) { for (auto multihex : item->GetMultihexMesh()) { find_include_multihex_lines(multihex); } } vector<ptr<ItemHexView>> sorted_target_items = vec_sorted(target_items, [](ptr<ItemHexView> i1, ptr<ItemHexView> i2) { return i1->GetId() < i2->GetId(); }); vector<ptr<ItemHexView>> sorted_source_items = vec_sorted(source_items, [](ptr<ItemHexView> i1, ptr<ItemHexView> i2) { return i2->GetId() < i1->GetId(); }); if (skip_selected) { sorted_target_items = vec_filter(sorted_target_items, [&](ptr<ItemHexView> i) { return !SelectedEntitiesSet.contains(i); }); sorted_source_items = vec_filter(sorted_source_items, [&](ptr<ItemHexView> i) { return !SelectedEntitiesSet.contains(i); }); } nptr<ItemHexView> best_target_item; nptr<ItemHexView> best_source_item; if (!sorted_target_items.empty()) { best_target_item = sorted_target_items.front(); } if (!sorted_source_items.empty()) { best_source_item = sorted_source_items.front(); } if (best_target_item && (!best_source_item || best_target_item->GetId() < item->GetId())) { source_item = item; target_item = best_target_item; } else if (best_source_item) { source_item = best_source_item; target_item = item; } } else if (item->GetMultihexGeneration() == MultihexGenerationType::AnyUnique) { span<refcount_ptr<ItemHexView>> map_items = map->GetItems(); for (size_t check_index = map_items.size(); check_index > 0; check_index--) { auto check_item = map_items[check_index - 1].as_ptr(); if (check_item->GetProtoId() != item->GetProtoId()) { continue; } if (skip_selected && SelectedEntitiesSet.contains(check_item)) { continue; } if (CompareMultihexItemForMerge(check_item, item, false)) { if (item->GetId() < check_item->GetId()) { source_item = check_item; target_item = item; } else { source_item = item; target_item = check_item; } break; } } } // Merge item and his mesh to another item mesh if (source_item && target_item) { MergeItemToMultihexMesh(map, source_item, target_item); return target_item; } return nullptr; } void MapperEngine::MergeItemToMultihexMesh(ptr<MapView> map, ptr<ItemHexView> source_item, ptr<ItemHexView> target_item) { FO_STACK_TRACE_ENTRY(); auto multihex_mesh = target_item->GetMultihexMesh(); bool some_hex_added = false; if (source_item->GetHex() != target_item->GetHex() && vec_safe_add_unique_value(multihex_mesh, source_item->GetHex())) { some_hex_added = true; } if (source_item->IsNonEmptyMultihexMesh()) { auto source_multihex_mesh = source_item->GetMultihexMesh(); multihex_mesh.reserve(multihex_mesh.size() + source_multihex_mesh.size()); for (auto multihex : source_multihex_mesh) { if (multihex != target_item->GetHex() && vec_safe_add_unique_value(multihex_mesh, multihex)) { some_hex_added = true; } } } if (some_hex_added) { target_item->SetMultihexMesh(multihex_mesh); map->RefreshItem(target_item, true); } map->DestroyItem(source_item); } void MapperEngine::FindMultihexMeshForItemAroundHex(ptr<MapView> map, ptr<ItemHexView> item, mpos hex, bool merge_to_it, unordered_set<ptr<ItemHexView>>& result) const { FO_STACK_TRACE_ENTRY(); auto find_mergable_item_on_hex = [&](mpos check_hex) -> nptr<ItemHexView> { if (!map->GetSize().is_valid_pos(check_hex)) { return nullptr; } for (ptr<ItemHexView> check_item : map->GetItemsOnHex(check_hex)) { if (result.contains(check_item)) { continue; } if (check_item->GetProtoId() != item->GetProtoId()) { continue; } if (CompareMultihexItemForMerge(merge_to_it ? check_item : item, merge_to_it ? item : check_item, true)) { return check_item; } } return nullptr; }; // At same hex if (auto mergable_item = find_mergable_item_on_hex(hex)) { result.emplace(mergable_item); } // Neighbor hexes for (int32_t i = 0; i < GameSettings::MAP_DIR_COUNT; i++) { if (mpos hex2 = hex; GeometryHelper::MoveHexByDir(hex2, hdir(i), map->GetSize())) { if (auto mergable_item = find_mergable_item_on_hex(hex2)) { result.emplace(mergable_item); } } } } auto MapperEngine::CompareMultihexItemForMerge(ptr<const ItemHexView> source_item, ptr<const ItemHexView> target_item, bool allow_clean_merge) const -> bool { FO_STACK_TRACE_ENTRY(); if (source_item->GetId() == target_item->GetId()) { return false; } if (source_item->GetProtoId() != target_item->GetProtoId()) { return false; } // Our data is not modified (same as in proto) small_vector<ptr<const Property>, 2> ignore_props {source_item->GetPropertyHex(), source_item->GetPropertyMultihexMesh()}; if (allow_clean_merge) { if (source_item->GetProperties()->CompareData(*source_item->GetProto()->GetProperties(), ignore_props, true)) { return true; } } // Our data is same as checked item if (source_item->GetProperties()->CompareData(*target_item->GetProperties(), ignore_props, true)) { return true; } return false; } auto MapperEngine::BreakItemsMultihexMeshes(ptr<MapView> map) -> size_t { FO_STACK_TRACE_ENTRY(); size_t breaks = 0; vector<refcount_ptr<ItemHexView>> items = to_vector(map->GetItems()); for (size_t i = 0; i < items.size(); i++) { auto item = items[i].as_ptr(); if (!item->IsNonEmptyMultihexMesh()) { continue; } auto multihex_mesh = item->GetMultihexMesh(); item->SetMultihexMesh({}); for (auto multihex : multihex_mesh) { map->AddMapperItem(item->GetProtoId(), multihex, item->GetProperties()); breaks++; } map->RefreshItem(item, true); } map->DefferedRefreshItems(); return breaks; } auto MapperEngine::TryBreakItemFromMultihexMesh(ptr<MapView> map, ptr<ItemHexView> item, mpos hex) -> nptr<ItemHexView> { FO_STACK_TRACE_ENTRY(); if (!item->IsNonEmptyMultihexMesh()) { return item; } auto multihex_lines = item->GetMultihexLines(); auto check_hex_for_hit = [&](mpos item_hex) { if (item_hex == hex) { return true; } if (!multihex_lines.empty()) { bool found = false; GeometryHelper::ForEachMultihexLines(multihex_lines, item_hex, map->GetSize(), [&](mpos line_hex) { found = found || line_hex == hex; }); return found; } return false; }; auto multihex_mesh = item->GetMultihexMesh(); if (check_hex_for_hit(item->GetHex())) { item->SetMultihexMesh({}); auto separated_item = map->AddMapperItem(item->GetProtoId(), item->GetHex(), item->GetProperties()); auto new_mesh_holder_hex = multihex_mesh.front(); vec_remove_unique_value(multihex_mesh, new_mesh_holder_hex); item->SetMultihexMesh(multihex_mesh); map->MoveItem(item, new_mesh_holder_hex); return separated_item; } else { for (auto multihex : multihex_mesh) { if (check_hex_for_hit(multihex)) { item->SetMultihexMesh({}); auto separated_item = map->AddMapperItem(item->GetProtoId(), multihex, item->GetProperties()); vec_remove_unique_value(multihex_mesh, multihex); item->SetMultihexMesh(multihex_mesh); map->RefreshItem(item, true); return separated_item; } } return nullptr; } } void MapperEngine::BufferCopy() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); BufferRawHex = cur_map->GetScreenRawHex(); EntitiesBuffer.clear(); // Add entities to buffer function<void(EntityBuf&, ptr<ClientEntity>)> add_entity; add_entity = [&add_entity, this](EntityBuf& entity_buf, ptr<ClientEntity> entity) { mpos hex; auto cr = entity.dyn_cast<CritterHexView>(); auto item = entity.dyn_cast<ItemHexView>(); auto entity_with_proto = entity.dyn_cast<EntityWithProto>(); FO_VERIFY_AND_THROW(entity_with_proto, "Buffered entity does not have an associated prototype"); if (cr) { hex = cr->GetHex(); } else if (item) { hex = item->GetHex(); } entity_buf.Hex = hex; entity_buf.IsCritter = !!cr; entity_buf.IsItem = !!item; entity_buf.Proto = entity_with_proto->GetProto(); entity_buf.Props.emplace(entity->GetProperties()->Copy()); vector<refcount_ptr<ItemView>> children = GetEntityInnerItems(entity); for (size_t i = 0; i < children.size(); i++) { auto child_buf = SafeAlloc::MakeUnique<EntityBuf>(); add_entity(*child_buf, children[i]); entity_buf.Children.emplace_back(std::move(child_buf)); } }; for (size_t i = 0; i < SelectedEntities.size(); i++) { EntitiesBuffer.emplace_back(); add_entity(EntitiesBuffer.back(), SelectedEntities[i]); } } void MapperEngine::BufferCut() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } BufferCopy(); SelectDelete(); } void MapperEngine::BufferPaste() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); vector<EntityBuf> pasted_entities; ipos32 screen_raw_hex = cur_map->GetScreenRawHex(); int32_t hx_offset = screen_raw_hex.x - BufferRawHex.x; int32_t hy_offset = screen_raw_hex.y - BufferRawHex.y; SelectClear(); for (const auto& entity_buf : EntitiesBuffer) { int32_t raw_hx = numeric_cast<int32_t>(entity_buf.Hex.x) + hx_offset; int32_t raw_hy = numeric_cast<int32_t>(entity_buf.Hex.y) + hy_offset; if (!cur_map->GetSize().is_valid_pos(raw_hx, raw_hy)) { continue; } mpos hex = cur_map->GetSize().from_raw_pos(raw_hx, raw_hy); function<void(const EntityBuf&, ptr<ItemView>)> add_item_inner_items; add_item_inner_items = [&add_item_inner_items, &cur_map](const EntityBuf& item_entity_buf, ptr<ItemView> item) { for (const auto& child_buf : item_entity_buf.Children) { auto inner_item = item->AddMapperInnerItem(cur_map->GenTempEntityId(), child_buf->Proto.dyn_cast<const ProtoItem>(), {}, child_buf->GetProps()); add_item_inner_items(*child_buf, inner_item); } }; if (entity_buf.IsCritter) { auto cr = cur_map->AddMapperCritter(entity_buf.Proto->GetProtoId(), hex, mdir(), entity_buf.GetProps()); for (const auto& child_buf : entity_buf.Children) { auto inv_item = cr->AddMapperInvItem(cur_map->GenTempEntityId(), child_buf->Proto.dyn_cast<const ProtoItem>(), CritterItemSlot::Inventory, child_buf->GetProps()); add_item_inner_items(*child_buf, inv_item); } SelectAdd(cr); pasted_entities.emplace_back(); CaptureEntityBuf(pasted_entities.back(), cr); } else if (entity_buf.IsItem) { auto item = cur_map->AddMapperItem(entity_buf.Proto->GetProtoId(), hex, entity_buf.GetProps()); add_item_inner_items(entity_buf, item); SelectAdd(item); pasted_entities.emplace_back(); CaptureEntityBuf(pasted_entities.back(), item); } } if (!pasted_entities.empty()) { PushUndoOp(GetCurMap(), UndoOp {"Paste selection", [pasted_entities](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { for (auto it = pasted_entities.rbegin(); it != pasted_entities.rend(); ++it) { if (auto target = mapper->FindEntityById(*active_map, it->Id)) { mapper->DeleteEntity(target); } } return true; }, [pasted_entities](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { for (const auto& entity_buf : pasted_entities) { if (!mapper->RestoreEntityBuf(entity_buf)) { return false; } } mapper->SetMapDirty(*active_map); return true; }}); } } auto MapperEngine::JumpHistoryToIndex(int32_t target_index) -> bool { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return false; } auto ctx = GetUndoContext(GetCurMap(), false); int32_t total_count = ctx ? numeric_cast<int32_t>(ctx->UndoStack.size() + ctx->RedoStack.size()) : 0; target_index = std::clamp(target_index, 0, total_count); while (true) { auto cur_ctx = GetUndoContext(GetCurMap(), false); int32_t applied_count = cur_ctx ? numeric_cast<int32_t>(cur_ctx->UndoStack.size()) : 0; if (applied_count == target_index) { return true; } if (applied_count > target_index) { if (!ExecuteUndo()) { return false; } } else { if (!ExecuteRedo()) { return false; } } } } void MapperEngine::DrawHistoryWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!HistoryWindowVisible) { return; } ImGui::SetNextWindowPos({48.0f, 48.0f}, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({640.0f, 760.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("History", &HistoryWindowVisible, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::End(); return; } if (!_curMap) { ImGui::TextUnformatted("No map loaded"); ImGui::End(); return; } auto ctx = GetUndoContext(GetCurMap(), false); int32_t undo_count = ctx ? numeric_cast<int32_t>(ctx->UndoStack.size()) : 0; int32_t redo_count = ctx ? numeric_cast<int32_t>(ctx->RedoStack.size()) : 0; int32_t total_count = undo_count + redo_count; auto& last_history_map = LastHistoryMap; int32_t& last_history_undo_count = LastHistoryUndoCount; bool scroll_to_current = ImGui::IsWindowAppearing() || !(last_history_map == GetCurMap()) || last_history_undo_count != undo_count; last_history_map = GetCurMap(); last_history_undo_count = undo_count; ImGui::Text("Applied: %d / %d", undo_count, total_count); ImGui::Separator(); auto draw_history_button = [&](string_view label, int32_t target_index, bool is_current) { string button_label = is_current ? strex("%s [current]", label).str() : string(label); if (is_current) { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); } bool clicked = ImGui::Button(button_label.c_str(), {-FLT_MIN, 0.0f}); if (is_current) { ImGui::PopStyleColor(2); } if (clicked && !is_current) { JumpHistoryToIndex(target_index); } }; if (ImGui::BeginChild("HistoryEntries", {0.0f, 0.0f}, false)) { if (undo_count == 0) { draw_history_button("Initial state", 0, true); } else { draw_history_button("Initial state", 0, false); } if (ctx && undo_count > 0) { ImGui::SeparatorText("Undo"); for (int32_t index = 0; index < undo_count; index++) { string label = strex("{}. {}", index + 1, ctx->UndoStack[numeric_cast<size_t>(index)].Label).str(); draw_history_button(label, index + 1, false); } } ImGui::SeparatorText("Current position"); ImGui::TextDisabled("Applied commands: %d", undo_count); if (scroll_to_current) { ImGui::SetScrollHereY(0.5f); } if (ctx && redo_count > 0) { ImGui::SeparatorText("Redo"); for (int32_t index = 0; index < redo_count; index++) { int32_t redo_index = redo_count - 1 - index; int32_t target_index = undo_count + index + 1; string label = strex("{}. {}", target_index, ctx->RedoStack[numeric_cast<size_t>(redo_index)].Label).str(); draw_history_button(label, target_index, false); } } ImGui::EndChild(); } ImGui::End(); } void MapperEngine::CurDraw() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } bool can_place_object = CurMode == CUR_MODE_PLACE_OBJECT && ((IsItemMode() && ActiveItemProtos && !ActiveItemProtos->empty()) || (IsCritMode() && ActiveCritterProtos && !ActiveCritterProtos->empty())); if (!can_place_object) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (IsItemMode() && ActiveItemProtos && !ActiveItemProtos->empty()) { const auto& proto = (*ActiveItemProtos)[GetActiveProtoIndex()]; mpos hex; if (!cur_map->GetHexAtScreen(MousePos, hex, nullptr)) { return; } if (proto->GetIsTile()) { hex = cur_map->GetSize().from_raw_pos(hex.x - hex.x % Settings->MapTileStep, hex.y - hex.y % Settings->MapTileStep); } auto spr = GetPreviewSprite(proto->GetPicMap()); if (spr) { float32_t zoom = cur_map->GetSpritesZoom(); ipos32 pos = cur_map->MapToScreenPos(cur_map->GetHexMapPos(hex)); pos += ipos32(iround<int32_t>(numeric_cast<float32_t>(proto->GetOffset().x) * zoom), iround<int32_t>(numeric_cast<float32_t>(proto->GetOffset().y) * zoom)); pos += ipos32(iround<int32_t>(numeric_cast<float32_t>(spr->GetOffset().x) * zoom), iround<int32_t>(numeric_cast<float32_t>(spr->GetOffset().y) * zoom)); pos += ipos32(iround<int32_t>(numeric_cast<float32_t>(Settings->MapHexWidth / 2) * zoom), iround<int32_t>(numeric_cast<float32_t>(Settings->MapHexHeight) * zoom)); pos -= ipos32(iround<int32_t>(numeric_cast<float32_t>(spr->GetSize().width / 2) * zoom), iround<int32_t>(numeric_cast<float32_t>(spr->GetSize().height) * zoom)); if (proto->GetIsTile() && PreviewRoofTiles) { // The flat tile/roof XY offset already came from the prototype Offset above; a roof preview rides the // same 3D elevation as a placed roof tile, so raise it on screen by that elevation's projection. float32_t elev_y = GeometryHelper::ProjectWorldToMap(vec3 {0.0F, numeric_cast<float32_t>(Settings->MapRoofElevation), 0.0F}).y; pos.y += iround<int32_t>(elev_y * zoom); } int32_t width = iround<int32_t>(numeric_cast<float32_t>(spr->GetSize().width) * zoom); int32_t height = iround<int32_t>(numeric_cast<float32_t>(spr->GetSize().height) * zoom); SprMngr.DrawSpriteSize(spr, pos, {width, height}, true, false, Color::Neutral); } return; } if (IsCritMode() && ActiveCritterProtos && !ActiveCritterProtos->empty()) { auto model_name = (*ActiveCritterProtos)[GetActiveProtoIndex()]->GetModelName(); auto anim = ResMngr.GetCritterPreviewSpr(model_name, CritterStateAnim::Unarmed, CritterActionAnim::Idle, CritterDir, nullptr); mpos hex; if (!cur_map->GetHexAtScreen(MousePos, hex, nullptr)) { return; } float32_t zoom = cur_map->GetSpritesZoom(); ipos32 pos = cur_map->MapToScreenPos(cur_map->GetHexMapPos(hex)); pos += ipos32(iround<int32_t>(numeric_cast<float32_t>(anim->GetOffset().x) * zoom), iround<int32_t>(numeric_cast<float32_t>(anim->GetOffset().y) * zoom)); pos -= ipos32(iround<int32_t>(numeric_cast<float32_t>(anim->GetSize().width / 2) * zoom), iround<int32_t>(numeric_cast<float32_t>(anim->GetSize().height) * zoom)); int32_t width = iround<int32_t>(numeric_cast<float32_t>(anim->GetSize().width) * zoom); int32_t height = iround<int32_t>(numeric_cast<float32_t>(anim->GetSize().height) * zoom); SprMngr.DrawSpriteSize(anim, pos, {width, height}, true, false, Color::Neutral); } } void MapperEngine::DrawSettingsWindowImGui() { FO_STACK_TRACE_ENTRY(); if (!SettingsWindowVisible) { return; } ImGui::SetNextWindowPos({96.0f, 96.0f}, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({420.0f, 320.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("Settings", &SettingsWindowVisible, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::End(); return; } auto apply_resolution = [&](isize32 resolution) { if (Settings->ScreenWidth == resolution.width && Settings->ScreenHeight == resolution.height) { return; } SprMngr.SetScreenSize(resolution); SprMngr.SetWindowSize(resolution); UpdateLocalConfigValue(Cache, "ScreenWidth", strex("{}", resolution.width)); UpdateLocalConfigValue(Cache, "ScreenHeight", strex("{}", resolution.height)); AddMess(strex("Resolution changed to {}x{}", resolution.width, resolution.height)); }; ImGui::Text("Current resolution: %d x %d", Settings->ScreenWidth, Settings->ScreenHeight); bool fullscreen = SprMngr.IsFullscreen(); if (ImGui::Checkbox("Fullscreen", &fullscreen)) { SprMngr.ToggleFullscreen(); } ImGui::Separator(); if (ImGui::Button("Reset layout")) { ResetImGuiSettingsRequested = true; } ImGui::Separator(); ImGui::TextUnformatted("Popular resolutions"); constexpr array<isize32, 10> popular_resolutions {{ {1024, 768}, {1280, 720}, {1280, 800}, {1366, 768}, {1440, 900}, {1600, 900}, {1680, 1050}, {1920, 1080}, {2560, 1440}, {3840, 2160}, }}; if (ImGui::BeginChild("##SettingsResolutions", {0.0f, 0.0f}, false)) { for (const auto& res : popular_resolutions) { bool is_current = Settings->ScreenWidth == res.width && Settings->ScreenHeight == res.height; if (ImGui::Selectable(strex("{} x {}", res.width, res.height).c_str(), is_current)) { apply_resolution(res); } } } ImGui::EndChild(); ImGui::End(); } void MapperEngine::CurRMouseUp() { FO_STACK_TRACE_ENTRY(); if (MouseHoldMode == INT_PAN) { if (RightMouseDragged) { nanotime now_time = nanotime::now(); float32_t sample_ms = (now_time - RightMouseVelocityTime).to_ms<float32_t>(); if ((RightMouseVelocityAccum.x != 0.0f || RightMouseVelocityAccum.y != 0.0f) && sample_ms > 0.0f) { RightMouseInertia = RightMouseVelocityAccum * (1000.0f / sample_ms); } RightMouseVelocityAccum = {}; RightMouseVelocityTime = {}; } else { RightMouseInertia = {}; RightMouseVelocityAccum = {}; RightMouseVelocityTime = {}; if (CurMode == CUR_MODE_PLACE_OBJECT) { SetCurMode(CUR_MODE_DEFAULT); } if (!SelectedEntities.empty()) { SelectClear(); } } MouseHoldMode = INT_NONE; RightMouseDragged = false; } } void MapperEngine::CurMMouseDown() { FO_STACK_TRACE_ENTRY(); if (SelectedEntities.empty()) { CritterDir = GetNextCritterDir(CritterDir); PreviewRoofTiles = !PreviewRoofTiles; } else { for (size_t i = 0; i < SelectedEntities.size(); i++) { if (auto cr = SelectedEntities[i].dyn_cast<CritterHexView>()) { AdvanceCritterDir(cr); } } } } void MapperEngine::SetCurMode(int cur_mode) { FO_STACK_TRACE_ENTRY(); CurMode = cur_mode; // Restore alpha for (size_t i = 0; i < SelectedEntities.size(); i++) { if (auto hex_view = SelectedEntities[i].dyn_cast<HexView>()) { if (CurMode == CUR_MODE_MOVE_SELECTION) { hex_view->RestoreAlpha(); } else { hex_view->SetTargetAlpha(SelectAlpha); } } } } auto MapperEngine::IsCurInRect(const irect32& rect, int32_t ax, int32_t ay) const -> bool { FO_STACK_TRACE_ENTRY(); return MousePos.x >= rect.x + ax && MousePos.y >= rect.y + ay && MousePos.x < rect.x + rect.width + ax && MousePos.y < rect.y + rect.height + ay; } auto MapperEngine::IsCurInRect(const irect32& rect) const -> bool { FO_STACK_TRACE_ENTRY(); return MousePos.x >= rect.x && MousePos.y >= rect.y && MousePos.x < rect.x + rect.width && MousePos.y < rect.y + rect.height; } auto MapperEngine::IsCurInInterface() const -> bool { FO_STACK_TRACE_ENTRY(); return IsImGuiMouseCaptured(); } auto MapperEngine::GetCurHex(mpos& hex, bool ignore_interface) -> bool { FO_STACK_TRACE_ENTRY(); hex = {}; if (!ignore_interface && IsCurInInterface()) { return false; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); return cur_map->GetHexAtScreen(MousePos, hex, nullptr); } void MapperEngine::DrawConsoleImGui() { FO_STACK_TRACE_ENTRY(); if (!ConsoleEdit) { return; } float32_t base_y = numeric_cast<float32_t>(MainPanelPos.y + MainPanelWindowRect.height + MAPPER_CONSOLE_WINDOW_OFFSET.y); float32_t base_x = numeric_cast<float32_t>(MainPanelPos.x + MAPPER_CONSOLE_WINDOW_OFFSET.x); ImGui::SetNextWindowPos({base_x, base_y}, ImGuiCond_Always); ImGui::SetNextWindowBgAlpha(0.85f); bool keep_open = ConsoleEdit; int32_t window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings; if (ImGui::Begin("Mapper Console", &keep_open, window_flags)) { if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); } if (ImGuiInputTextString("##MapperConsoleInput", ConsoleStr, ImGuiInputTextFlags_EnterReturnsTrue)) { ConsoleSubmitCommand(); } if (ImGui::IsItemActive()) { auto input_state = make_nptr(ImGui::GetInputTextState(ImGui::GetItemID())); if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { if (ConsoleHistoryCur > 0) { ConsoleHistoryCur--; ConsoleStr = ConsoleHistory[ConsoleHistoryCur]; if (input_state) { input_state->ReloadUserBufAndMoveToEnd(); } } } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { if (ConsoleHistoryCur + 1 >= numeric_cast<int32_t>(ConsoleHistory.size())) { ConsoleHistoryCur = numeric_cast<int32_t>(ConsoleHistory.size()); ConsoleStr.clear(); } else { ConsoleHistoryCur++; ConsoleStr = ConsoleHistory[ConsoleHistoryCur]; } if (input_state) { input_state->ReloadUserBufAndMoveToEnd(); } } } if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { ConsoleEdit = false; ConsoleStr.clear(); } } ImGui::End(); if (!keep_open) { ConsoleEdit = false; ConsoleStr.clear(); } } void MapperEngine::ConsoleSubmitCommand() { FO_STACK_TRACE_ENTRY(); if (ConsoleStr.empty()) { ConsoleEdit = false; return; } // Keep history unique and bounded. ConsoleHistory.emplace_back(ConsoleStr); for (int32_t i = 0; i < numeric_cast<int32_t>(ConsoleHistory.size()) - 1; i++) { if (ConsoleHistory[i] == ConsoleHistory[ConsoleHistory.size() - 1]) { ConsoleHistory.erase(ConsoleHistory.begin() + i); i = -1; } } while (numeric_cast<int32_t>(ConsoleHistory.size()) > Settings->ConsoleHistorySize) { ConsoleHistory.erase(ConsoleHistory.begin()); } ConsoleHistoryCur = numeric_cast<int32_t>(ConsoleHistory.size()); string history_str; for (const auto& str : ConsoleHistory) { history_str += str + "\n"; } Cache.SetString("mapper_console.txt", history_str); bool process_command = OnMapperMessage.Fire(ConsoleStr) == EventResult::ContinueChain; AddMess(ConsoleStr); if (process_command) { ParseCommand(ConsoleStr); } ConsoleEdit = false; ConsoleStr = ""; } void MapperEngine::ParseCommand(string_view command) { FO_STACK_TRACE_ENTRY(); if (command.empty()) { return; } // Load map if (command[0] == '~') { string_view map_name = strvex(command.substr(1)).trim(); if (map_name.empty()) { AddMess("Error parse map name"); return; } if (auto map = LoadMap(map_name)) { AddMess("Load map success"); ShowMap(map); } else { AddMess("Load map failed"); } } // Save map else if (command[0] == '^') { string_view map_name = strvex(command.substr(1)).trim(); if (map_name.empty()) { AddMess("Error parse map name"); return; } if (!_curMap) { AddMess("Map not loaded"); return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); SaveMap(cur_map, map_name); AddMess("Save map success"); } // Run script else if (command[0] == '#') { string before_snapshot = _curMap && !UndoRedoInProgress ? CaptureMapSnapshot(GetCurMap()) : string {}; string command_str = string(command.substr(1)); istringstream icmd(command_str); string func_name; if (!(icmd >> func_name)) { AddMess("Function name not typed"); return; } auto func = FindFunc<string, string>(Hashes.ToHashedString(func_name)); if (!func) { AddMess("Function not found"); return; } string str = strvex(command).substring_after(' ').trim().str(); if (!func.Call(str)) { AddMess("Script execution fail"); return; } if (!before_snapshot.empty()) { string after_snapshot = CaptureMapSnapshot(GetCurMap()); if (before_snapshot != after_snapshot) { auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); string map_name = string(cur_map->GetName()); PushUndoOp(GetCurMap(), UndoOp {strex("Script {}", func_name), [map_name, before_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, before_snapshot); }, [map_name, after_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, after_snapshot); }, true}); } } AddMess(strex("Result: {}", func.GetResult())); } // Critter animations else if (command[0] == '@') { AddMess("Playing critter animations"); if (!_curMap) { AddMess("Map not loaded"); return; } vector<int32_t> anims = strvex(command.substr(1)).split_to_int32(' '); if (anims.empty()) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); if (!SelectedEntities.empty()) { for (size_t i = 0; i < SelectedEntities.size(); i++) { if (auto cr = SelectedEntities[i].dyn_cast<CritterHexView>()) { cr->StopAnim(); for (size_t j = 0; j < anims.size() / 2; j++) { cr->AppendAnim(static_cast<CritterStateAnim>(anims[numeric_cast<size_t>(j) * 2]), static_cast<CritterActionAnim>(anims[j * 2 + 1]), nullptr); } } } } else { span<refcount_ptr<CritterHexView>> critters = cur_map->GetCritters(); for (size_t i = 0; i < critters.size(); i++) { auto cr = critters[i].as_ptr(); cr->StopAnim(); for (size_t j = 0; j < anims.size() / 2; j++) { cr->AppendAnim(static_cast<CritterStateAnim>(anims[numeric_cast<size_t>(j) * 2]), static_cast<CritterActionAnim>(anims[j * 2 + 1]), nullptr); } } } } // Other else if (command[0] == '*') { string icommand_str = string(command.substr(1)); istringstream icommand(icommand_str); string command_ext; if (!(icommand >> command_ext)) { return; } if (command_ext == "new") { auto registrar = GetPropertyRegistrar(MapProperties::ENTITY_TYPE_NAME); FO_VERIFY_AND_THROW(registrar, "Map property registrar is not available"); auto pmap = SafeAlloc::MakeRefCounted<ProtoMap>(Hashes.ToHashedString("new"), registrar); pmap->SetSize({GameSettings::DEFAULT_MAP_SIZE, GameSettings::DEFAULT_MAP_SIZE}); auto map = SafeAlloc::MakeRefCounted<MapView>(this, ident_t {}, pmap, GetApp()->MainWindow.GetSize()); map->EnableMapperMode(); map->SetScrollCheck(false); map->InstantScrollTo({GameSettings::DEFAULT_MAP_SIZE / 2, GameSettings::DEFAULT_MAP_SIZE / 2}); LoadedMaps.emplace_back(std::move(map)); ShowMap(LoadedMaps.back()); SetMapDirty(LoadedMaps.back()); AddMess("Create map success"); } else if (command_ext == "unload" && _curMap) { AddMess("Unload map"); auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); UnloadMap(cur_map); if (!LoadedMaps.empty()) { ShowMap(LoadedMaps.front()); } } else if (command_ext == "size" && _curMap) { AddMess("Resize map"); int32_t maxhx = 0; int32_t maxhy = 0; if (!(icommand >> maxhx >> maxhy)) { AddMess("Invalid args"); return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); ResizeMap(cur_map, maxhx, maxhy); } else if (command_ext == "resave") { AddMess("Resave maps"); auto map_files = MapsFileSys.FilterFiles(""); for (const auto& map_file_header : map_files) { if (!IsProtoFileExtension(map_file_header.GetPath())) { continue; } File map_file = File::Load(map_file_header); auto declared_maps = MapLoader::EnumerateMaps(map_file.GetPath(), map_file.GetStr()); for (const auto& map_name : declared_maps) { if (auto map = LoadMap(map_name)) { SaveMap(map, map_name); AddMess(strex("Resave map: {}", map_name)); } else { AddMess(strex("Failed to load map: {}", map_name)); } } } } else if (command_ext == "reverse-light" && _curMap) { string before_snapshot = !UndoRedoInProgress ? CaptureMapSnapshot(GetCurMap()) : string {}; auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); span<refcount_ptr<ItemHexView>> items = cur_map->GetItems(); for (size_t i = 0; i < items.size(); i++) { auto item = items[i].as_ptr(); uint32_t rgba = item->GetLightColor().rgba; rgba = (rgba & 0xFF000000) | ((rgba & 0xFF) << 16) | (rgba & 0xFF00) | ((rgba & 0xFF0000) >> 16); item->SetLightColor(ucolor(rgba)); } cur_map->RebuildMap(); SetMapDirty(GetCurMap()); if (!before_snapshot.empty()) { string after_snapshot = CaptureMapSnapshot(GetCurMap()); if (before_snapshot != after_snapshot) { string map_name = string(cur_map->GetName()); PushUndoOp(GetCurMap(), UndoOp {"Reverse lights", [map_name, before_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, before_snapshot); }, [map_name, after_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, after_snapshot); }, true}); } } } else if (command_ext == "merge-items" && _curMap) { string before_snapshot = !UndoRedoInProgress ? CaptureMapSnapshot(GetCurMap()) : string {}; auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); MergeItemsToMultihexMeshes(cur_map); size_t merge_items_repeat_count = MergeItemsToMultihexMeshes(cur_map); FO_VERIFY_AND_THROW(merge_items_repeat_count == 0, "Mapper merge-items command is not idempotent for current map", cur_map->GetName(), merge_items_repeat_count); SetMapDirty(GetCurMap()); if (!before_snapshot.empty()) { string after_snapshot = CaptureMapSnapshot(GetCurMap()); if (before_snapshot != after_snapshot) { string map_name = string(cur_map->GetName()); PushUndoOp(GetCurMap(), UndoOp {"Merge items", [map_name, before_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, before_snapshot); }, [map_name, after_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, after_snapshot); }, true}); } } } else if (command_ext == "break-items" && _curMap) { string before_snapshot = !UndoRedoInProgress ? CaptureMapSnapshot(GetCurMap()) : string {}; auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); BreakItemsMultihexMeshes(cur_map); size_t break_items_repeat_count = BreakItemsMultihexMeshes(cur_map); FO_VERIFY_AND_THROW(break_items_repeat_count == 0, "Mapper break-items command is not idempotent for current map", cur_map->GetName(), break_items_repeat_count); SetMapDirty(GetCurMap()); if (!before_snapshot.empty()) { string after_snapshot = CaptureMapSnapshot(GetCurMap()); if (before_snapshot != after_snapshot) { string map_name = string(cur_map->GetName()); PushUndoOp(GetCurMap(), UndoOp {"Break items", [map_name, before_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, before_snapshot); }, [map_name, after_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, after_snapshot); }, true}); } } } } else { AddMess("Unknown command"); } } auto MapperEngine::IsProtoFileExtension(string_view path) const -> bool { FO_STACK_TRACE_ENTRY(); string ext = strex(path).get_file_extension(); return std::ranges::find(Settings->ProtoFileExtensions, ext) != Settings->ProtoFileExtensions.end(); } auto MapperEngine::LoadMapFromText(string_view map_name, string_view file_name, const string& map_text) -> nptr<MapView> { FO_STACK_TRACE_ENTRY(); auto map_data = ConfigFile(map_text, ConfigFileOption::SkipNestedSections); if (!map_data.HasSection("ProtoMap")) { throw MapLoaderException("Invalid map format", map_name, file_name); } string file_stem = strex(file_name).extract_file_name().erase_file_extension().str(); auto anchor_sections = map_data.GetSections("ProtoMap"); nptr<const map<string_view, string_view>> found_anchor_section; for (const auto& anchor_kv : anchor_sections) { auto anchor_name_it = anchor_kv->find("$Name"); bool anchor_matched = anchor_name_it != anchor_kv->end() ? anchor_name_it->second == map_name : file_stem == map_name; if (anchor_matched) { found_anchor_section = anchor_kv; break; } } if (!found_anchor_section) { throw MapLoaderException("Map is not declared in the file", map_name, file_name); } const auto& proto_map_section = *found_anchor_section; map<string, string> proto_map_header_extra_fields; for (const auto& [key, value] : proto_map_section) { if (key.starts_with("$Text")) { proto_map_header_extra_fields.emplace(string {key}, string {value}); } } auto registrar = GetPropertyRegistrar(MapProperties::ENTITY_TYPE_NAME); FO_VERIFY_AND_THROW(registrar, "Map property registrar is not available"); auto pmap = SafeAlloc::MakeRefCounted<ProtoMap>(Hashes.ToHashedString(map_name), registrar); pmap->GetPropertiesForEdit()->ApplyFromText(proto_map_section); auto new_map = SafeAlloc::MakeRefCounted<MapView>(this, ident_t {}, pmap, GetApp()->MainWindow.GetSize()); new_map->SetHeaderExtraFields(std::move(proto_map_header_extra_fields)); new_map->EnableMapperMode(); new_map->SetScrollCheck(false); try { new_map->LoadFromFile(map_name, file_name, map_text); } catch (const MapLoaderException& ex) { AddMess(strex("Map truncated: {}", ex.what())); return nullptr; } MergeItemsToMultihexMeshes(new_map); size_t load_merge_repeat_count = MergeItemsToMultihexMeshes(new_map); FO_VERIFY_AND_THROW(load_merge_repeat_count == 0, "Loaded map merge-items normalization is not idempotent", map_name, load_merge_repeat_count); new_map->InstantScrollTo(new_map->GetWorkHex()); OnEditMapLoad.Fire(new_map); LoadedMaps.emplace_back(std::move(new_map)); auto loaded_map = LoadedMaps.back().as_nptr(); auto undo_context = GetUndoContext(loaded_map, true); FO_VERIFY_AND_THROW(undo_context, "Undo context is null"); undo_context->CleanUndoDepth = 0; SetMapDirty(loaded_map, false); return loaded_map; } auto MapperEngine::LoadMap(string_view map_name) -> nptr<MapView> { FO_STACK_TRACE_ENTRY(); auto map_files = MapsFileSys.FilterFiles(""); string requested_stem = strex(map_name).extract_file_name().erase_file_extension().str(); File map_file; string resolved_map_name; auto resolve_declared_map = [&](const File& candidate) -> bool { auto declared_maps = MapLoader::EnumerateMaps(candidate.GetPath(), candidate.GetStr()); if (std::ranges::find(declared_maps, string {map_name}) != declared_maps.end()) { resolved_map_name = string {map_name}; return true; } if (std::ranges::find(declared_maps, requested_stem) != declared_maps.end()) { resolved_map_name = requested_stem; return true; } return false; }; if (File by_name = map_files.FindFileByName(map_name); by_name && resolve_declared_map(by_name)) { map_file = std::move(by_name); } if (!map_file) { string map_path = strex(map_name).format_path().str(); for (const auto& proto_ext : Settings->ProtoFileExtensions) { File by_path = map_files.FindFileByPath(strex("{}.{}", map_path, proto_ext).str()); if (by_path && resolve_declared_map(by_path)) { map_file = std::move(by_path); break; } } } if (!map_file) { // The map may live in a multi-map file with an unrelated name for (const auto& file_header : map_files) { if (!IsProtoFileExtension(file_header.GetPath())) { continue; } File candidate_file = File::Load(file_header); if (resolve_declared_map(candidate_file)) { map_file = std::move(candidate_file); break; } } } if (!map_file) { AddMess("Map file not found"); return nullptr; } return LoadMapFromText(resolved_map_name, map_file.GetPath(), map_file.GetStr()); } void MapperEngine::ShowMap(ptr<MapView> map) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(!map->IsDestroyed(), "Mapper cannot show a destroyed map", map->GetName(), LoadedMaps.size()); auto it = std::ranges::find_if(LoadedMaps, [map](const refcount_ptr<MapView>& loaded_map) noexcept { auto loaded_map_view = loaded_map.as_nptr(); nptr<const MapView> target_map = map; return loaded_map_view == target_map; }); FO_VERIFY_AND_THROW(it != LoadedMaps.end(), "Mapper show requested for a map that is not tracked as loaded", map->GetName(), LoadedMaps.size()); WorkspaceWindowVisible = true; MapListWindowVisible = false; nptr<MapView> map_view = map; if (!(GetCurMap() == map_view)) { ParticleEditors.OnCurrentMapChanging(map_view); if (_curMap) { SelectClear(); } _curMap = map.hold_ref(); ClearMapperTrackOverlay(); RefreshActiveProtoLists(); } } auto MapperEngine::IsMapDirty(nptr<MapView> map) const -> bool { FO_STACK_TRACE_ENTRY(); if (!map) { return false; } return DirtyMaps.contains(map); } void MapperEngine::SetMapDirty(nptr<MapView> map, bool dirty) { FO_STACK_TRACE_ENTRY(); if (!map) { return; } if (dirty) { DirtyMaps.emplace(map); } else { DirtyMaps.erase(map); } } void MapperEngine::SaveCurrentMap() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); SaveMap(cur_map, ""); AddMess(strex("Saved map: {}", cur_map->GetName())); } void MapperEngine::ResetCurrentMapChanges() { FO_STACK_TRACE_ENTRY(); if (!_curMap) { return; } auto cur_map = GetCurMap(); FO_VERIFY_AND_THROW(cur_map, "Current map is null"); string map_name = string(cur_map->GetName()); ClearUndoContext(cur_map); UnloadMap(cur_map); if (auto map = LoadMap(map_name)) { ShowMap(map); AddMess(strex("Reset changes: {}", map_name)); } else { AddMess(strex("Reset changes failed: {}", map_name)); } } // Rebuilds a multi-map map-file content with one map's sections replaced by freshly saved text. // Sections of other maps are preserved byte-exact; the saved map's block lands at the position // of its first original section. static auto SpliceMapIntoFomapContent(string_view file_stem, const string& original_content, string_view map_name, string_view map_content) -> string { FO_STACK_TRACE_ENTRY(); // Locate section runs: each starts at its [..] header line and spans up to the next header struct FomapSectionRange { size_t Start {}; size_t End {}; string Name {}; }; vector<FomapSectionRange> section_ranges; size_t line_begin = 0; while (line_begin < original_content.length()) { size_t line_end = original_content.find('\n', line_begin); if (line_end == string::npos) { line_end = original_content.length(); } string_view line = strvex(string_view(original_content).substr(line_begin, line_end - line_begin)).trim(); if (!line.empty() && line.front() == '[') { size_t name_end = line.find(']'); if (name_end != string_view::npos) { if (!section_ranges.empty()) { section_ranges.back().End = line_begin; } section_ranges.emplace_back(FomapSectionRange {line_begin, original_content.length(), string(strvex(line.substr(1, name_end - 1)).trim())}); } } line_begin = line_end + 1; } // Anchors own themselves and set the [$Name/X] context; the anchor identity is its $Name or the file stem auto resolve_anchor_name = [&](const FomapSectionRange& range) -> string { size_t anchor_line_begin = range.Start; while (anchor_line_begin < range.End) { size_t anchor_line_end = original_content.find('\n', anchor_line_begin); if (anchor_line_end == string::npos || anchor_line_end > range.End) { anchor_line_end = range.End; } string_view anchor_line = strvex(string_view(original_content).substr(anchor_line_begin, anchor_line_end - anchor_line_begin)).trim(); if (strvex(anchor_line).starts_with("$Name")) { string_view name_value = strvex(anchor_line.substr("$Name"_len)).trim(); if (!name_value.empty() && name_value.front() == '=') { return string(strvex(name_value.substr(1)).trim()); } } anchor_line_begin = anchor_line_end + 1; } return string(file_stem); }; string result; result.reserve(original_content.length() + map_content.length()); if (!section_ranges.empty()) { result.append(original_content, 0, section_ranges.front().Start); } else { result.append(original_content); } string context_name = string(file_stem); bool map_written = false; for (const auto& range : section_ranges) { string owner_name; if (range.Name.find('/') == string::npos) { owner_name = resolve_anchor_name(range); context_name = owner_name; } else if (strvex(range.Name).starts_with("$Name/")) { owner_name = context_name; } else { owner_name = range.Name.substr(0, range.Name.find('/')); } if (owner_name == map_name) { if (!map_written) { result.append(map_content); map_written = true; } } else { result.append(original_content, range.Start, range.End - range.Start); } } if (!map_written) { result.append(map_content); } return result; } void MapperEngine::SaveMap(ptr<MapView> map, string_view custom_name) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(!map->IsDestroyed(), "Mapper cannot save a destroyed map", map->GetName(), custom_name); MergeItemsToMultihexMeshes(map); size_t save_merge_repeat_count = MergeItemsToMultihexMeshes(map); FO_VERIFY_AND_THROW(save_merge_repeat_count == 0, "Map save merge-items normalization is not idempotent", map->GetName(), custom_name, save_merge_repeat_count); auto it = std::ranges::find_if(LoadedMaps, [map](const refcount_ptr<MapView>& loaded_map) noexcept { auto loaded_map_view = loaded_map.as_nptr(); nptr<const MapView> target_map = map; return loaded_map_view == target_map; }); FO_VERIFY_AND_THROW(it != LoadedMaps.end(), "Mapper save requested for a map that is not tracked as loaded", map->GetName(), custom_name, LoadedMaps.size()); string fomap_name = !custom_name.empty() ? string(custom_name) : string(map->GetProto()->GetName()); FO_VERIFY_AND_THROW(!fomap_name.empty(), "Mapper cannot determine a map name for saving", map->GetName(), custom_name, map->GetProto()->GetName()); string fomap_content = map->SaveToText(fomap_name); string fomap_path; string final_content; auto map_files = MapsFileSys.FilterFiles(""); // One pass over map containers: find the file that already declares this map, remember the // original map's file (save-as locality) and the first container (extension/dir reference) string original_map_name = string(map->GetProto()->GetName()); File declaring_file; vector<string> declaring_file_maps; string original_declaring_path; string first_container_path; for (const auto& file_header : map_files) { if (!IsProtoFileExtension(file_header.GetPath())) { continue; } File candidate_file = File::Load(file_header); auto declared_maps = MapLoader::EnumerateMaps(candidate_file.GetPath(), candidate_file.GetStr()); if (declared_maps.empty()) { continue; } if (first_container_path.empty()) { first_container_path = candidate_file.GetDiskPath(); } if (original_declaring_path.empty() && std::ranges::find(declared_maps, original_map_name) != declared_maps.end()) { original_declaring_path = candidate_file.GetDiskPath(); } if (std::ranges::find(declared_maps, fomap_name) != declared_maps.end()) { declaring_file = std::move(candidate_file); declaring_file_maps = std::move(declared_maps); break; } FO_VERIFY_AND_THROW(candidate_file.GetNameNoExt() != fomap_name, "Mapper save target file exists but does not declare the saved map", fomap_name, candidate_file.GetPath()); } if (declaring_file) { fomap_path = declaring_file.GetDiskPath(); if (declaring_file_maps.size() == 1) { final_content = fomap_content; } else { final_content = SpliceMapIntoFomapContent(declaring_file.GetNameNoExt(), declaring_file.GetStr(), fomap_name, fomap_content); } } else { final_content = fomap_content; // A brand-new map lands beside the original map's file, else beside any map container, // keeping that file's extension; with no containers at all the first configured // extension names the new file if (!original_declaring_path.empty()) { fomap_path = strex(original_declaring_path).change_file_name(fomap_name); } else if (!first_container_path.empty()) { fomap_path = strex(first_container_path).change_file_name(fomap_name); } else { FO_VERIFY_AND_THROW(!Settings->ProtoFileExtensions.empty(), "No proto file extensions are configured"); fomap_path = strex("{}.{}", fomap_name, Settings->ProtoFileExtensions.front()).format_path(); } } string dir = strex(fomap_path).extract_dir().str(); if (!dir.empty()) { bool dir_ok = fs_create_directories(dir); FO_VERIFY_AND_THROW(dir_ok, "Mapper failed to create the map output directory", dir, fomap_path, fomap_name); } std::ofstream fomap_file {std::filesystem::path {fs_make_path(fomap_path)}, std::ios::binary | std::ios::trunc}; FO_VERIFY_AND_THROW(fomap_file, "Mapper failed to open the map file for writing", fomap_path, fomap_name, final_content.size()); if (!final_content.empty()) { fomap_file.write(final_content.data(), static_cast<std::streamsize>(final_content.size())); } FO_VERIFY_AND_THROW(fomap_file, "Mapper failed to write the map file content", fomap_path, fomap_name, final_content.size()); MapBrowserNamesStale = true; OnEditMapSave.Fire(map); auto ctx = GetUndoContext(map, true); FO_VERIFY_AND_THROW(ctx, "Context is null"); ctx->CleanUndoDepth = numeric_cast<int32_t>(ctx->UndoStack.size()); SetMapDirty(map, false); } void MapperEngine::SaveMapToDir(ptr<MapView> map, string_view sub_dir, string_view name) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(!map->IsDestroyed(), "Cannot save a destroyed map"); FO_VERIFY_AND_THROW(!name.empty(), "Map save name is empty"); MergeItemsToMultihexMeshes(map); FO_VERIFY_AND_THROW(MergeItemsToMultihexMeshes(map) == 0, "Failed to merge items to multihex meshes before save"); auto it = std::ranges::find_if(LoadedMaps, [map](const refcount_ptr<MapView>& loaded_map) noexcept { auto loaded_map_view = loaded_map.as_nptr(); nptr<const MapView> target_map = map; return loaded_map_view == target_map; }); FO_VERIFY_AND_THROW(it != LoadedMaps.end(), "Map to save is not in the loaded maps list"); string fomap_content = map->SaveToText(name); // Resolve the on-disk Maps root from an existing map container's disk path, then write the // new map under <MapsRoot>/<sub_dir>/<name>.<ext> with the reference container's extension. // The AI authoring loop targets a checked-in Maps/Generated/ area, so this avoids SaveMap's // "first file's directory" fallback that could scatter generated maps next to unrelated content. auto map_files = MapsFileSys.FilterFiles(""); string reference_map_path; for (const auto& file_header : map_files) { if (!IsProtoFileExtension(file_header.GetPath())) { continue; } File candidate_file = File::Load(file_header); if (!MapLoader::EnumerateMaps(candidate_file.GetPath(), candidate_file.GetStr()).empty()) { reference_map_path = candidate_file.GetDiskPath(); break; } } FO_VERIFY_AND_THROW(!reference_map_path.empty(), "No map container found to resolve the maps root directory"); string reference_map_ext = strex(reference_map_path).get_file_extension().str(); string maps_root = strex(reference_map_path).extract_dir().str(); std::ranges::replace(maps_root, '\\', '/'); if (auto pos = maps_root.rfind("/Maps/"); pos != string::npos) { maps_root = maps_root.substr(0, pos + 5); // keep ".../Maps" } // else: best-effort fallback keeps the reference file's directory (incl. a path already ending in /Maps) string target_dir = !sub_dir.empty() ? strex("{}/{}", maps_root, sub_dir).str() : maps_root; string fomap_path = strex("{}/{}.{}", target_dir, name, reference_map_ext).format_path().str(); string dir = strex(fomap_path).extract_dir().str(); if (!dir.empty()) { bool dir_ok = fs_create_directories(dir); FO_VERIFY_AND_THROW(dir_ok, "Unable to create the target map directory", dir); } std::ofstream fomap_file {std::filesystem::path {fs_make_path(fomap_path)}, std::ios::binary | std::ios::trunc}; FO_VERIFY_AND_THROW(fomap_file, "Unable to open the fomap file for writing", fomap_path); if (!fomap_content.empty()) { fomap_file.write(fomap_content.data(), static_cast<std::streamsize>(fomap_content.size())); } FO_VERIFY_AND_THROW(fomap_file, "Unable to write the fomap file", fomap_path); MapBrowserNamesStale = true; OnEditMapSave.Fire(map); auto ctx = GetUndoContext(map, true); FO_VERIFY_AND_THROW(ctx, "Context is null"); ctx->CleanUndoDepth = numeric_cast<int32_t>(ctx->UndoStack.size()); SetMapDirty(map, false); } void MapperEngine::UnloadMap(ptr<MapView> map, bool clear_undo) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(!map->IsDestroyed(), "Mapper cannot unload a destroyed map", map->GetName(), clear_undo); nptr<MapView> map_view = map; ParticleEditors.OnMapUnloading(map); if (GetCurMap() == map_view) { SelectClear(); ClearMapperTrackOverlay(); _curMap.reset(); } auto it = std::ranges::find_if(LoadedMaps, [map](const refcount_ptr<MapView>& loaded_map) noexcept { auto loaded_map_view = loaded_map.as_nptr(); nptr<const MapView> target_map = map; return loaded_map_view == target_map; }); FO_VERIFY_AND_THROW(it != LoadedMaps.end(), "Mapper unload requested for a map that is not tracked as loaded", map->GetName(), LoadedMaps.size(), clear_undo); SetMapDirty(map, false); if (clear_undo) { ClearUndoContext(map); } map->DestroySelf(); LoadedMaps.erase(it); } void MapperEngine::ResizeMap(ptr<MapView> map, int32_t width, int32_t height) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(!map->IsDestroyed(), "Map is already destroyed"); string before_snapshot = !UndoRedoInProgress ? CaptureMapSnapshot(map) : string {}; int32_t corrected_width = std::clamp(width, GameSettings::MIN_MAP_SIZE, GameSettings::MAX_MAP_SIZE); int32_t corrected_height = std::clamp(height, GameSettings::MIN_MAP_SIZE, GameSettings::MAX_MAP_SIZE); map->Resize(msize(numeric_cast<int16_t>(corrected_width), numeric_cast<int16_t>(corrected_height))); map->InstantScrollTo(map->GetWorkHex()); nptr<MapView> map_view = map; if (GetCurMap() == map_view) { SelectClear(); ClearMapperTrackOverlay(); } SetMapDirty(map); string after_snapshot = CaptureMapSnapshot(map); if (!before_snapshot.empty() && before_snapshot != after_snapshot) { PushUndoOp(map, UndoOp {"Resize map", [map_name = string(map->GetName()), before_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, before_snapshot); }, [map_name = string(map->GetName()), after_snapshot](ptr<MapperEngine> mapper, ptr<ptr<MapView>> active_map) { return mapper->RestoreMapSnapshot(active_map, map_name, after_snapshot); }, true}); } } void MapperEngine::AddMess(string_view message_text) { FO_STACK_TRACE_ENTRY(); string str = strex("- {}\n", message_text); time_desc_t time = nanotime::now().desc(true); string mess_time = strex("{:02}:{:02}:{:02} ", time.hour, time.minute, time.second); MessBox.emplace_back(MessBoxMessage {.Type = 0, .Mess = str, .Time = mess_time}); } auto MapperEngine::GetEntityInnerItems(ptr<ClientEntity> entity) const -> vector<refcount_ptr<ItemView>> { FO_STACK_TRACE_ENTRY(); if (auto cr = entity.dyn_cast<CritterView>()) { span<refcount_ptr<ItemView>> items = cr->GetInvItems(); return {items.begin(), items.end()}; } if (auto item = entity.dyn_cast<ItemView>()) { span<refcount_ptr<ItemView>> items = item->GetInnerItems(); return {items.begin(), items.end()}; } return {}; } auto MapperEngine::MakeRectFromEdges(int32_t left, int32_t top, int32_t right, int32_t bottom) const -> irect32 { FO_STACK_TRACE_ENTRY(); return {left, top, right - left, bottom - top}; } auto MapperEngine::ShiftDayTimeWithWrap(int32_t day_time, int32_t delta_minutes) const -> int32_t { FO_STACK_TRACE_ENTRY(); day_time += delta_minutes; while (day_time > DAY_TIME_VISIBLE_UPPER_BOUND) { day_time -= DAY_TIME_WRAP_MINUTES; } while (day_time < 0) { day_time += DAY_TIME_WRAP_MINUTES; } return day_time; } auto MapperEngine::ScaleZoomValue(float32_t current_zoom, float32_t factor) const -> float32_t { FO_STACK_TRACE_ENTRY(); return std::clamp(current_zoom * factor, GameSettings::MIN_ZOOM, GameSettings::MAX_ZOOM); } auto MapperEngine::GetTileLayerFromKey(KeyCode key) const -> optional<int32_t> { FO_STACK_TRACE_ENTRY(); switch (key) { case KeyCode::C0: case KeyCode::Numpad0: return 0; case KeyCode::C1: case KeyCode::Numpad1: return 1; case KeyCode::C2: case KeyCode::Numpad2: return 2; case KeyCode::C3: case KeyCode::Numpad3: return 3; case KeyCode::C4: case KeyCode::Numpad4: return 4; default: return std::nullopt; } } auto MapperEngine::GetNextCritterDir(mdir dir) const -> mdir { FO_STACK_TRACE_ENTRY(); return dir.incHex(); } void MapperEngine::AdvanceCritterDir(ptr<CritterHexView> cr) const { FO_STACK_TRACE_ENTRY(); cr->ChangeDir(GetNextCritterDir(cr->GetDir())); } void MapperEngine::ToggleMapVisibilityFlag(nptr<MapView> map, bool& value) const { FO_STACK_TRACE_ENTRY(); value = !value; if (map) { map->RebuildMap(); } } auto MapperEngine::ContainsCaseInsensitive(string_view text, string_view filter) const -> bool { FO_STACK_TRACE_ENTRY(); if (filter.empty()) { return true; } string lower_text = strex(text).lower_utf8(); string lower_filter = strex(filter).lower_utf8(); return lower_text.find(lower_filter) != string::npos; } auto MapperEngine::ResolveAtlasSprite(nptr<const Sprite> sprite) const -> nptr<const AtlasSprite> { FO_STACK_TRACE_ENTRY(); auto source_sprite = sprite; if (auto sprite_sheet = source_sprite.dyn_cast<const SpriteSheet>()) { source_sprite = sprite_sheet->GetCurSpr(); } return source_sprite.dyn_cast<const AtlasSprite>(); } auto MapperEngine::DrawAtlasSpriteImage(ptr<ImDrawList> draw_list, ptr<const AtlasSprite> atlas_sprite, ImVec2 logical_min, ImVec2 logical_size) const -> bool { FO_STACK_TRACE_ENTRY(); auto texture = atlas_sprite->GetBatchTexture(); if (!texture) { return false; } optional<AtlasSpriteRegion> region = atlas_sprite->ResolveRegion({}, {1.0f, 1.0f}, {logical_min.x, logical_min.y, logical_size.x, logical_size.y}); if (!region.has_value()) { return false; } const frect32& draw_rect = region->DrawRect; const frect32& texture_rect = region->TextureRect; draw_list->AddImage(make_nptr(texture.get()).void_cast(), {draw_rect.x, draw_rect.y}, {draw_rect.x + draw_rect.width, draw_rect.y + draw_rect.height}, {texture_rect.x, texture_rect.y}, {texture_rect.x + texture_rect.width, texture_rect.y + texture_rect.height}); return true; } auto MapperEngine::GetInspectorValueType(ptr<const Property> prop) const -> AnyData::ValueType { FO_STACK_TRACE_ENTRY(); if (prop->IsString() || prop->IsArrayOfString() || prop->IsDictOfString() || prop->IsDictOfArrayOfString() || prop->IsBaseTypeHash() || prop->IsBaseTypeEnum() || prop->IsBaseTypeComplexStruct()) { return AnyData::ValueType::String; } if (prop->IsBaseTypeInt()) { return AnyData::ValueType::Int64; } if (prop->IsBaseTypeBool()) { return AnyData::ValueType::Bool; } if (prop->IsBaseTypeFloat()) { return AnyData::ValueType::Float64; } return AnyData::ValueType::String; } auto MapperEngine::ParseInspectorValue(ptr<const Property> prop, string_view text) const -> optional<AnyData::Value> { FO_STACK_TRACE_ENTRY(); try { return AnyData::ParseValue(string(text), false, prop->IsArray(), GetInspectorValueType(prop)); } catch (const std::exception&) { return std::nullopt; } } auto MapperEngine::MakeDefaultInspectorArrayElement(ptr<const Property> prop) const -> AnyData::Value { FO_STACK_TRACE_ENTRY(); switch (GetInspectorValueType(prop)) { case AnyData::ValueType::Int64: return AnyData::Value {int64_t {0}}; case AnyData::ValueType::Float64: return AnyData::Value {float64_t {0.0}}; case AnyData::ValueType::Bool: return AnyData::Value {false}; case AnyData::ValueType::String: return AnyData::Value {string {}}; default: FO_UNREACHABLE_PLACE(); } } auto MapperEngine::SerializeInspectorArray(vector<AnyData::Value> entries) const -> string { FO_STACK_TRACE_ENTRY(); AnyData::Array value_arr; value_arr.Reserve(entries.size()); for (auto& entry : entries) { value_arr.EmplaceBack(std::move(entry)); } return AnyData::ValueToString(AnyData::Value {std::move(value_arr)}); } auto MapperEngine::SerializeInspectorStringArray(const vector<string>& entries) const -> string { FO_STACK_TRACE_ENTRY(); vector<AnyData::Value> values; values.reserve(entries.size()); for (const auto& entry : entries) { values.emplace_back(string {entry}); } return SerializeInspectorArray(std::move(values)); } auto MapperEngine::GetInspectorStructLayout(ptr<const Property> prop) const -> nptr<const StructLayoutDesc> { FO_STACK_TRACE_ENTRY(); const auto& base_type = prop->GetBaseType(); if (base_type.StructLayout && (base_type.IsComplexStruct || base_type.IsSimpleStruct) && base_type.StructLayout->Fields.size() > 1) { return base_type.StructLayout; } return nullptr; } auto MapperEngine::ReadInspectorToken(nptr<const char> str, string& result) const -> nptr<const char> { FO_STACK_TRACE_ENTRY(); if (str[0] == 0) { return nullptr; } auto decode_char = [str](size_t char_pos, size_t& char_len) { char_len = utf8::DecodeStrNtLen(&str[char_pos]); utf8::Decode(&str[char_pos], char_len); }; size_t pos = 0; size_t length = 0; decode_char(pos, length); while (length == 1 && (str[pos] == ' ' || str[pos] == '\t')) { pos++; decode_char(pos, length); } if (str[pos] == 0) { return nullptr; } size_t begin; if (length == 1 && str[pos] == '"') { pos++; begin = pos; while (str[pos] != 0) { if (length == 1 && str[pos] == '\\') { pos++; if (str[pos] != 0) { decode_char(pos, length); pos += length; } } else if (length == 1 && str[pos] == '"') { break; } else { pos += length; } decode_char(pos, length); } } else { begin = pos; while (str[pos] != 0) { if (length == 1 && str[pos] == '\\') { pos++; decode_char(pos, length); pos += length; } else if (length == 1 && (str[pos] == ' ' || str[pos] == '\t')) { break; } else { pos += length; } decode_char(pos, length); } } auto next_token = make_ptr(&str[pos + (str[pos] != 0 ? 1 : 0)]); result.assign(&str[begin], pos - begin); return next_token; } auto MapperEngine::ParseInspectorStructFields(const StructLayoutDesc& layout, string_view text) const -> optional<vector<string>> { FO_STACK_TRACE_ENTRY(); try { string text_str = string {text}; auto token_pos = make_nptr(text_str.c_str()); string token; vector<string> fields; fields.reserve(layout.Fields.size()); while ((token_pos = ReadInspectorToken(token_pos, token))) { fields.emplace_back(StringEscaping::DecodeString(token)); } if (fields.size() != layout.Fields.size()) { return std::nullopt; } return fields; } catch (const std::exception&) { return std::nullopt; } } auto MapperEngine::ParseInspectorStringEntries(string_view text) const -> optional<vector<string>> { FO_STACK_TRACE_ENTRY(); try { auto parsed = AnyData::ParseValue(string(text), false, true, AnyData::ValueType::String); if (parsed.Type() != AnyData::ValueType::Array) { return std::nullopt; } vector<string> entries; entries.reserve(parsed.AsArray().Size()); for (const auto& entry : parsed.AsArray()) { entries.emplace_back(entry.AsString()); } return entries; } catch (const std::exception&) { return std::nullopt; } } auto MapperEngine::GetImGuiInputTextStringUserData(nptr<void> user_data) -> ptr<ImGuiInputTextStringUserData> { FO_NO_STACK_TRACE_ENTRY(); auto typed_user_data = user_data.reinterpret_as<ImGuiInputTextStringUserData>(); IM_ASSERT(typed_user_data); return typed_user_data; } int MapperEngine::ImGuiInputTextStringCallback(ImGuiInputTextCallbackData* data) { FO_STACK_TRACE_ENTRY(); IM_ASSERT(data); ptr<ImGuiInputTextCallbackData> callback_data = data; auto user_data = GetImGuiInputTextStringUserData(callback_data->UserData); if (callback_data->EventFlag == ImGuiInputTextFlags_CallbackResize) { ptr<string> str = user_data->Value; IM_ASSERT(callback_data->Buf == str->c_str()); str->resize(callback_data->BufTextLen); callback_data->Buf = str->data(); } else if (callback_data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) { if (user_data->LatinOnly && callback_data->EventChar >= 128) { return 1; } } else if (callback_data->EventFlag == ImGuiInputTextFlags_CallbackAlways) { if (user_data->MoveCaretToEnd) { callback_data->CursorPos = callback_data->BufTextLen; callback_data->SelectionStart = callback_data->BufTextLen; callback_data->SelectionEnd = callback_data->BufTextLen; user_data->MoveCaretToEnd = false; } } return 0; } auto MapperEngine::ImGuiInputTextString(ptr<const char> label, string& value, ImGuiInputTextFlags flags, bool latin_only, bool move_caret_to_end) const -> bool { FO_STACK_TRACE_ENTRY(); return ImGuiInputTextStringImpl(label, nullptr, value, flags, latin_only, move_caret_to_end); } auto MapperEngine::ImGuiInputTextStringWithHint(ptr<const char> label, ptr<const char> hint, string& value, ImGuiInputTextFlags flags, bool latin_only, bool move_caret_to_end) const -> bool { FO_STACK_TRACE_ENTRY(); return ImGuiInputTextStringImpl(label, hint, value, flags, latin_only, move_caret_to_end); } auto MapperEngine::ImGuiInputTextStringImpl(ptr<const char> label, nptr<const char> hint, string& value, ImGuiInputTextFlags flags, bool latin_only, bool move_caret_to_end) const -> bool { FO_STACK_TRACE_ENTRY(); if (value.capacity() == 0) { value.reserve(256); } auto value_ptr = make_ptr(&value); ImGuiInputTextStringUserData user_data {.Value = value_ptr, .LatinOnly = latin_only, .MoveCaretToEnd = move_caret_to_end}; flags |= ImGuiInputTextFlags_CallbackResize; if (latin_only) { flags |= ImGuiInputTextFlags_CallbackCharFilter; } if (move_caret_to_end) { flags |= ImGuiInputTextFlags_CallbackAlways; } if (hint) { return ImGui::InputTextWithHint(label.get(), hint.get(), value.data(), value.capacity() + 1, flags, ImGuiInputTextStringCallback, &user_data); } return ImGui::InputText(label.get(), value.data(), value.capacity() + 1, flags, ImGuiInputTextStringCallback, &user_data); } auto MapperEngine::IsInspectorValueSameAsProto(ptr<const Entity> entity, ptr<const Property> prop, string_view value_text) const -> bool { FO_STACK_TRACE_ENTRY(); auto entity_with_proto = entity.dyn_cast<const EntityWithProto>(); if (!entity_with_proto) { return true; } try { return entity_with_proto->GetProto()->GetProperties()->SavePropertyToText(prop) == value_text; } catch (const std::exception&) { return true; } } void MapperEngine::UpdateLocalConfigValue(CacheStorage& cache, string_view key, string_view value) const { FO_STACK_TRACE_ENTRY(); string cfg_user; if (cache.HasEntry(LOCAL_CONFIG_NAME)) { auto config = ConfigFile(cache.GetString(LOCAL_CONFIG_NAME)); const auto& sections = config.GetSections(); bool wrote_root_key = false; bool has_root_section = false; for (const auto& [section_name, key_values] : *sections) { if (!section_name.empty()) { cfg_user += strex("[{}]\n", section_name); } else { has_root_section = true; } for (const auto& [entry_key, entry_value] : key_values) { if (section_name.empty() && entry_key == key) { cfg_user += strex("{} = {}\n", key, value); wrote_root_key = true; } else { cfg_user += strex("{} = {}\n", entry_key, entry_value); } } if (section_name.empty() && !wrote_root_key) { cfg_user += strex("{} = {}\n", key, value); wrote_root_key = true; } if (!section_name.empty()) { cfg_user += "\n"; } } if (!has_root_section) { cfg_user = strex("{} = {}\n", key, value).str() + cfg_user; } } else { cfg_user = strex("{} = {}\n", key, value); } cache.SetString(LOCAL_CONFIG_NAME, cfg_user); } FO_END_NAMESPACE