diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b8ae1711..72954eedd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2285,13 +2285,22 @@ if(CLIENT) component.h editor.cpp editor.h + editor_action.h + editor_actions.cpp + editor_actions.h + editor_history.cpp + editor_history.h editor_object.cpp editor_object.h + editor_props.cpp + editor_trackers.cpp + editor_trackers.h explanations.cpp map_grid.cpp map_grid.h map_view.cpp map_view.h + mapitems.h mapitems/envelope.cpp mapitems/envelope.h mapitems/image.cpp diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 536ed952d..e016280f5 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -248,6 +248,7 @@ MACRO_CONFIG_INT(ClRefreshRateInactive, cl_refresh_rate_inactive, 120, 0, 10000, MACRO_CONFIG_INT(ClEditor, cl_editor, 0, 0, 1, CFGFLAG_CLIENT, "Open the map editor") MACRO_CONFIG_INT(ClEditorDilate, cl_editor_dilate, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Automatically dilates embedded images") MACRO_CONFIG_STR(ClSkinFilterString, cl_skin_filter_string, 25, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Skin filtering string") +MACRO_CONFIG_INT(ClEditorMaxHistory, cl_editor_max_history, 50, 1, 500, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Maximum number of undo actions in the editor history (not shared between editor, envelope editor and server settings editor)") MACRO_CONFIG_INT(ClAutoDemoRecord, cl_auto_demo_record, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Automatically record demos") MACRO_CONFIG_INT(ClAutoDemoOnConnect, cl_auto_demo_on_connect, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Only start a new demo when connect while automatically record demos") diff --git a/src/engine/textrender.h b/src/engine/textrender.h index 9862d7a88..85fa991f0 100644 --- a/src/engine/textrender.h +++ b/src/engine/textrender.h @@ -140,6 +140,8 @@ MAYBE_UNUSED static const char *FONT_ICON_DICE_FIVE = "\xEF\x94\xA3"; MAYBE_UNUSED static const char *FONT_ICON_DICE_SIX = "\xEF\x94\xA6"; MAYBE_UNUSED static const char *FONT_ICON_LAYER_GROUP = "\xEF\x97\xBD"; +MAYBE_UNUSED static const char *FONT_ICON_UNDO = "\xEF\x8B\xAA"; +MAYBE_UNUSED static const char *FONT_ICON_REDO = "\xEF\x8B\xB9"; } // end namespace FontIcons enum ETextCursorSelectionMode diff --git a/src/game/client/ui.cpp b/src/game/client/ui.cpp index a02b4d238..4a47fe80b 100644 --- a/src/game/client/ui.cpp +++ b/src/game/client/ui.cpp @@ -490,19 +490,37 @@ int CUI::DoDraggableButtonLogic(const void *pID, int Checked, const CUIRect *pRe return ReturnValue; } -int CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY) +EEditState CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY) { + static const void *s_pEditing = nullptr; + if(MouseHovered(pRect)) SetHotItem(pID); + EEditState Res = EEditState::EDITING; + if(HotItem() == pID && MouseButtonClicked(0)) + { SetActiveItem(pID); + if(!s_pEditing) + { + s_pEditing = pID; + Res = EEditState::START; + } + } if(CheckActiveItem(pID) && !MouseButton(0)) + { SetActiveItem(nullptr); + if(s_pEditing == pID) + { + s_pEditing = nullptr; + Res = EEditState::END; + } + } - if(!CheckActiveItem(pID)) - return 0; + if(!CheckActiveItem(pID) && Res == EEditState::EDITING) + return EEditState::NONE; if(Input()->ShiftIsPressed()) m_MouseSlow = true; @@ -512,7 +530,7 @@ int CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float * if(pY) *pY = clamp(m_MouseY - pRect->y, 0.0f, pRect->h); - return 1; + return Res; } void CUI::DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp, float ScrollSpeed) @@ -981,12 +999,19 @@ int CUI::DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pTex } int64_t CUI::DoValueSelector(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props) +{ + return DoValueSelectorWithState(pID, pRect, pLabel, Current, Min, Max, Props).m_Value; +} + +SEditResult CUI::DoValueSelectorWithState(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props) { // logic static float s_Value; static CLineInputNumber s_NumberInput; static const void *s_pLastTextID = pID; const bool Inside = MouseInside(pRect); + static const void *s_pEditing = nullptr; + EEditState State = EEditState::NONE; if(Inside) SetHotItem(pID); @@ -1091,7 +1116,23 @@ int64_t CUI::DoValueSelector(const void *pID, const CUIRect *pRect, const char * if(!m_ValueSelectorTextMode) s_NumberInput.Clear(); - return Current; + if(s_pEditing == pID) + State = EEditState::EDITING; + + bool MouseLocked = CheckMouseLock(); + if((MouseLocked || m_ValueSelectorTextMode) && !s_pEditing) + { + State = EEditState::START; + s_pEditing = pID; + } + + if(!CheckMouseLock() && !m_ValueSelectorTextMode && s_pEditing == pID) + { + State = EEditState::END; + s_pEditing = nullptr; + } + + return SEditResult{State, Current}; } float CUI::DoScrollbarV(const void *pID, const CUIRect *pRect, float Current) @@ -1686,6 +1727,7 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View { SColorPickerPopupContext *pColorPicker = static_cast(pContext); CUI *pUI = pColorPicker->m_pUI; + pColorPicker->m_State = EEditState::NONE; CUIRect ColorsArea, HueArea, BottomArea, ModeButtonArea, HueRect, SatRect, ValueRect, HexRect, AlphaRect; @@ -1759,10 +1801,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View HuePartialArea.Draw4(TL, TL, BL, BL, IGraphics::CORNER_NONE, 0.0f); } - const auto &&RenderAlphaSelector = [&](unsigned OldA) -> unsigned { + const auto &&RenderAlphaSelector = [&](unsigned OldA) -> SEditResult { if(pColorPicker->m_Alpha) { - return pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[3], &AlphaRect, "A:", OldA, 0, 255); + return pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[3], &AlphaRect, "A:", OldA, 0, 255); } else { @@ -1770,7 +1812,7 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View str_format(aBuf, sizeof(aBuf), "A: %d", OldA); pUI->DoLabel(&AlphaRect, aBuf, 10.0f, TEXTALIGN_MC); AlphaRect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.65f), IGraphics::CORNER_ALL, 3.0f); - return OldA; + return {EEditState::NONE, OldA}; } }; @@ -1782,10 +1824,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View const unsigned OldV = (unsigned)(PickerColorHSV.v * 255.0f); const unsigned OldA = (unsigned)(PickerColorHSV.a * 255.0f); - const unsigned H = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); - const unsigned S = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); - const unsigned V = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "V:", OldV, 0, 255); - const unsigned A = RenderAlphaSelector(OldA); + const auto [StateH, H] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); + const auto [StateS, S] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); + const auto [StateV, V] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "V:", OldV, 0, 255); + const auto [StateA, A] = RenderAlphaSelector(OldA); if(OldH != H || OldS != S || OldV != V || OldA != A) { @@ -1793,6 +1835,15 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSL = color_cast(PickerColorHSV); PickerColorRGB = color_cast(PickerColorHSL); } + + for(auto State : {StateH, StateS, StateV, StateA}) + { + if(State != EEditState::NONE) + { + pColorPicker->m_State = State; + break; + } + } } else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_RGBA) { @@ -1801,10 +1852,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View const unsigned OldB = (unsigned)(PickerColorRGB.b * 255.0f); const unsigned OldA = (unsigned)(PickerColorRGB.a * 255.0f); - const unsigned R = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "R:", OldR, 0, 255); - const unsigned G = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "G:", OldG, 0, 255); - const unsigned B = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "B:", OldB, 0, 255); - const unsigned A = RenderAlphaSelector(OldA); + const auto [StateR, R] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "R:", OldR, 0, 255); + const auto [StateG, G] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "G:", OldG, 0, 255); + const auto [StateB, B] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "B:", OldB, 0, 255); + const auto [StateA, A] = RenderAlphaSelector(OldA); if(OldR != R || OldG != G || OldB != B || OldA != A) { @@ -1812,6 +1863,15 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSL = color_cast(PickerColorRGB); PickerColorHSV = color_cast(PickerColorHSL); } + + for(auto State : {StateR, StateG, StateB, StateA}) + { + if(State != EEditState::NONE) + { + pColorPicker->m_State = State; + break; + } + } } else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSLA) { @@ -1820,10 +1880,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View const unsigned OldL = (unsigned)(PickerColorHSL.l * 255.0f); const unsigned OldA = (unsigned)(PickerColorHSL.a * 255.0f); - const unsigned H = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); - const unsigned S = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); - const unsigned L = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "L:", OldL, 0, 255); - const unsigned A = RenderAlphaSelector(OldA); + const auto [StateH, H] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); + const auto [StateS, S] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); + const auto [StateL, L] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "L:", OldL, 0, 255); + const auto [StateA, A] = RenderAlphaSelector(OldA); if(OldH != H || OldS != S || OldL != L || OldA != A) { @@ -1831,6 +1891,15 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSV = color_cast(PickerColorHSL); PickerColorRGB = color_cast(PickerColorHSL); } + + for(auto State : {StateH, StateS, StateL, StateA}) + { + if(State != EEditState::NONE) + { + pColorPicker->m_State = State; + break; + } + } } else { @@ -1842,7 +1911,7 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View Props.m_IsHex = true; Props.m_HexPrefix = pColorPicker->m_Alpha ? 8 : 6; const unsigned OldHex = PickerColorRGB.PackAlphaLast(pColorPicker->m_Alpha); - const unsigned Hex = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[4], &HexRect, "Hex:", OldHex, 0, pColorPicker->m_Alpha ? 0xFFFFFFFFll : 0xFFFFFFll, Props); + auto [HexState, Hex] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[4], &HexRect, "Hex:", OldHex, 0, pColorPicker->m_Alpha ? 0xFFFFFFFFll : 0xFFFFFFll, Props); if(OldHex != Hex) { const float OldAlpha = PickerColorRGB.a; @@ -1853,21 +1922,28 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSV = color_cast(PickerColorHSL); } + if(HexState != EEditState::NONE) + pColorPicker->m_State = HexState; + // Logic float PickerX, PickerY; - if(pUI->DoPickerLogic(&pColorPicker->m_ColorPickerId, &ColorsArea, &PickerX, &PickerY)) + EEditState ColorPickerRes = pUI->DoPickerLogic(&pColorPicker->m_ColorPickerId, &ColorsArea, &PickerX, &PickerY); + if(ColorPickerRes != EEditState::NONE) { PickerColorHSV.y = PickerX / ColorsArea.w; PickerColorHSV.z = 1.0f - PickerY / ColorsArea.h; PickerColorHSL = color_cast(PickerColorHSV); PickerColorRGB = color_cast(PickerColorHSL); + pColorPicker->m_State = ColorPickerRes; } - if(pUI->DoPickerLogic(&pColorPicker->m_HuePickerId, &HueArea, &PickerX, &PickerY)) + EEditState HuePickerRes = pUI->DoPickerLogic(&pColorPicker->m_HuePickerId, &HueArea, &PickerX, &PickerY); + if(HuePickerRes != EEditState::NONE) { PickerColorHSV.x = 1.0f - PickerY / HueArea.h; PickerColorHSL = color_cast(PickerColorHSV); PickerColorRGB = color_cast(PickerColorHSL); + pColorPicker->m_State = HuePickerRes; } // Marker Color Area diff --git a/src/game/client/ui.h b/src/game/client/ui.h index 80a7e26df..324d4cff5 100644 --- a/src/game/client/ui.h +++ b/src/game/client/ui.h @@ -18,6 +18,22 @@ class IClient; class IGraphics; class IKernel; +enum class EEditState +{ + NONE, + START, + EDITING, + END, + ONE_GO +}; + +template +struct SEditResult +{ + EEditState m_State; + T m_Value; +}; + struct SUIAnimator { bool m_Active; @@ -488,7 +504,7 @@ public: int DoButtonLogic(const void *pID, int Checked, const CUIRect *pRect); int DoDraggableButtonLogic(const void *pID, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted); - int DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY); + EEditState DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY); void DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp = false, float ScrollSpeed = 10.0f); static vec2 CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, const float *pBiggestCharHeight = nullptr); @@ -505,6 +521,7 @@ public: int DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pText, const CUIRect *pRect, float Size, int Align, float Padding = 0.0f, bool TransparentInactive = false, bool Enabled = true); // value selector + SEditResult DoValueSelectorWithState(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props = {}); int64_t DoValueSelector(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props = {}); bool IsValueSelectorTextMode() const { return m_ValueSelectorTextMode; } void SetValueSelectorTextMode(bool TextMode) { m_ValueSelectorTextMode = TextMode; } @@ -620,6 +637,7 @@ public: const char m_ColorPickerId = 0; const char m_aValueSelectorIds[5] = {0}; CButtonContainer m_aModeButtons[(int)MODE_HSLA + 1]; + EEditState m_State; }; void ShowPopupColorPicker(float X, float Y, SColorPickerPopupContext *pContext); diff --git a/src/game/editor/auto_map.cpp b/src/game/editor/auto_map.cpp index f8cf86ed0..4f58f8e0f 100644 --- a/src/game/editor/auto_map.cpp +++ b/src/game/editor/auto_map.cpp @@ -9,6 +9,7 @@ #include "auto_map.h" #include "editor.h" // TODO: only needs CLayerTiles +#include "editor_actions.h" // Based on triple32inc from https://github.com/skeeto/hash-prospector/tree/79a6074062a84907df6e45b756134b74e2956760 static uint32_t HashUInt32(uint32_t Num) @@ -173,9 +174,9 @@ void CAutoMapper::Load(const char *pTileName) { Value = CPosRule::NOTINDEX; CIndexInfo NewIndexInfo1 = {0, 0, false}; - //CIndexInfo NewIndexInfo2 = {-1, 0}; + // CIndexInfo NewIndexInfo2 = {-1, 0}; vNewIndexList.push_back(NewIndexInfo1); - //vNewIndexList.push_back(NewIndexInfo2); + // vNewIndexList.push_back(NewIndexInfo2); } else if(!str_comp(aValue, "INDEX") || !str_comp(aValue, "NOTINDEX")) { @@ -425,8 +426,10 @@ void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigID, int Seed, { CTile *pIn = &pUpdateLayer->m_pTiles[(y - UpdateFromY) * pUpdateLayer->m_Width + x - UpdateFromX]; CTile *pOut = &pLayer->m_pTiles[y * pLayer->m_Width + x]; + CTile Previous = *pOut; pOut->m_Index = pIn->m_Index; pOut->m_Flags = pIn->m_Flags; + pLayer->RecordStateChange(x, y, Previous, *pOut); } } @@ -442,6 +445,7 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO Seed = rand(); CConfiguration *pConf = &m_vConfigs[ConfigID]; + pLayer->ClearHistory(); // for every run: copy tiles, automap, overwrite tiles for(size_t h = 0; h < pConf->m_vRuns.size(); ++h) @@ -534,8 +538,10 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO if(RespectRules && (pIndexRule->m_RandomProbability >= 1.0f || HashLocation(Seed, h, i, x + SeedOffsetX, y + SeedOffsetY) < HASH_MAX * pIndexRule->m_RandomProbability)) { + CTile Previous = *pTile; pTile->m_Index = pIndexRule->m_ID; pTile->m_Flags = pIndexRule->m_Flag; + pLayer->RecordStateChange(x, y, Previous, *pTile); } } } diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 073d693e2..5d002e85f 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -32,11 +32,13 @@ #include #include +#include #include #include #include "auto_map.h" #include "editor.h" +#include "editor_actions.h" #include #include @@ -328,7 +330,7 @@ void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, Graphics()->QuadsEnd(); } -int CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree, bool IsHex, int Corners, ColorRGBA *pColor, bool ShowValue) +SEditResult CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree, bool IsHex, int Corners, ColorRGBA *pColor, bool ShowValue) { // logic static float s_Value; @@ -337,6 +339,8 @@ int CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, in static void *s_pLastTextID = pID; const bool Inside = UI()->MouseInside(pRect); const int Base = IsHex ? 16 : 10; + static bool s_Editing = false; + EEditState State = EEditState::EDITING; if(UI()->MouseButton(1) && UI()->HotItem() == pID) { @@ -445,7 +449,20 @@ int CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, in if(!s_TextMode) s_NumberInput.Clear(); - return Current; + bool MouseLocked = UI()->CheckMouseLock(); + if((MouseLocked || s_TextMode) && !s_Editing) + { + State = EEditState::START; + s_Editing = true; + } + + if(!MouseLocked && !s_TextMode && s_Editing) + { + State = EEditState::END; + s_Editing = false; + } + + return SEditResult{State, Current}; } std::shared_ptr CEditor::GetSelectedGroup() const @@ -576,8 +593,14 @@ void CEditor::DeleteSelectedQuads() if(!pLayer) return; + std::vector vSelectedQuads(m_vSelectedQuads); + std::vector vDeletedQuads; + for(int i = 0; i < (int)m_vSelectedQuads.size(); ++i) { + auto const &Quad = pLayer->m_vQuads[m_vSelectedQuads[i]]; + vDeletedQuads.push_back(Quad); + pLayer->m_vQuads.erase(pLayer->m_vQuads.begin() + m_vSelectedQuads[i]); for(int j = i + 1; j < (int)m_vSelectedQuads.size(); ++j) if(m_vSelectedQuads[j] > m_vSelectedQuads[i]) @@ -586,6 +609,8 @@ void CEditor::DeleteSelectedQuads() m_vSelectedQuads.erase(m_vSelectedQuads.begin() + i); i--; } + + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], vSelectedQuads, vDeletedQuads)); } bool CEditor::IsQuadSelected(int Index) const @@ -714,6 +739,39 @@ bool CEditor::IsTangentSelected() const return IsTangentInSelected() || IsTangentOutSelected(); } +std::pair CEditor::EnvGetSelectedTimeAndValue() const +{ + if(m_SelectedEnvelope < 0 || m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size()) + return {}; + + std::shared_ptr pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; + int CurrentTime; + int CurrentValue; + if(IsTangentInSelected()) + { + auto [SelectedIndex, SelectedChannel] = m_SelectedTangentInPoint; + + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]; + CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel]; + } + else if(IsTangentOutSelected()) + { + auto [SelectedIndex, SelectedChannel] = m_SelectedTangentOutPoint; + + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]; + CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel]; + } + else + { + auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints.front(); + + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time; + CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; + } + + return std::pair{CurrentTime, CurrentValue}; +} + bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUser) { CEditor *pEditor = (CEditor *)pUser; @@ -1101,6 +1159,23 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); + // undo/redo group + TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_EditorHistory.CanUndo() - 1, &Button, 0, "[ctrl+z] Undo last action", IGraphics::CORNER_L)) + { + m_EditorHistory.Undo(); + } + + TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_EditorHistory.CanRedo() - 1, &Button, 0, "[ctrl+y] Redo last action", IGraphics::CORNER_R)) + { + m_EditorHistory.Redo(); + } + + TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); + // brush manipulation { int Enabled = m_pBrush->IsEmpty() ? -1 : 0; @@ -1144,7 +1219,8 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) } TB_Top.VSplitLeft(30.0f, &Button, &TB_Top); - s_RotationAmount = UiDoValueSelector(&s_RotationAmount, &Button, "", s_RotationAmount, TileLayer ? 90 : 1, 359, TileLayer ? 90 : 1, TileLayer ? 10.0f : 2.0f, "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", true, false, IGraphics::CORNER_NONE); + auto RotationAmountRes = UiDoValueSelector(&s_RotationAmount, &Button, "", s_RotationAmount, TileLayer ? 90 : 1, 359, TileLayer ? 90 : 1, TileLayer ? 10.0f : 2.0f, "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", true, false, IGraphics::CORNER_NONE); + s_RotationAmount = RotationAmountRes.m_Value; TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); static int s_CwButton = 0; @@ -1309,24 +1385,9 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) } if(pLayer->m_Type == LAYERTYPE_QUADS) - { - std::shared_ptr pLayerQuads = std::static_pointer_cast(pLayer); - - int Width = 64; - int Height = 64; - if(pLayerQuads->m_Image >= 0) - { - Width = m_Map.m_vpImages[pLayerQuads->m_Image]->m_Width; - Height = m_Map.m_vpImages[pLayerQuads->m_Image]->m_Height; - } - - pLayerQuads->NewQuad(x, y, Width, Height); - } + m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); else if(pLayer->m_Type == LAYERTYPE_SOUNDS) - { - std::shared_ptr pLayerSounds = std::static_pointer_cast(pLayer); - pLayerSounds->NewSource(x, y); - } + m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); } TB_Bottom.VSplitLeft(5.0f, &Button, &TB_Bottom); } @@ -1468,7 +1529,7 @@ void CEditor::DoSoundSource(CSoundSource *pSource, int Index) Graphics()->QuadsDraw(&QuadItem, 1); } -void CEditor::DoQuad(CQuad *pQuad, int Index) +void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index) { enum { @@ -1529,6 +1590,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) // check if we only should move pivot if(s_Operation == OP_MOVE_PIVOT) { + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); float x = wx; float y = wy; if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) @@ -1537,7 +1599,6 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) int OffsetX = f2fx(x) - pQuad->m_aPoints[4].x; int OffsetY = f2fx(y) - pQuad->m_aPoints[4].y; - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); for(auto &Selected : m_vSelectedQuads) { CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; @@ -1547,6 +1608,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) } else if(s_Operation == OP_MOVE_ALL) { + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); // move all points including pivot float x = wx; float y = wy; @@ -1556,7 +1618,6 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) int OffsetX = f2fx(x) - pQuad->m_aPoints[4].x; int OffsetY = f2fx(y) - pQuad->m_aPoints[4].y; - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); for(auto &Selected : m_vSelectedQuads) { CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; @@ -1569,7 +1630,8 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) } else if(s_Operation == OP_ROTATE) { - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) { CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]]; @@ -1624,6 +1686,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) UI()->DisableMouseLock(); s_Operation = OP_NONE; UI()->SetActiveItem(nullptr); + m_QuadTracker.EndQuadTrack(); } else if(UI()->MouseButton(1)) { @@ -1632,7 +1695,6 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) UI()->SetActiveItem(nullptr); // Reset points to old position - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) { CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]]; @@ -1652,6 +1714,10 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) else SelectQuad(Index); } + else if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL) + { + m_QuadTracker.EndQuadTrack(); + } UI()->DisableMouseLock(); s_Operation = OP_NONE; @@ -1668,7 +1734,6 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) s_Operation = OP_ROTATE; s_RotateAngle = 0; - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); s_vvRotatePoints.clear(); s_vvRotatePoints.resize(m_vSelectedQuads.size()); for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) @@ -1727,7 +1792,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) Graphics()->QuadsDraw(&QuadItem, 1); } -void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) +void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int V) { void *pID = &pQuad->m_aPoints[V]; @@ -1786,6 +1851,8 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) if(s_Operation == OP_MOVEPOINT) { + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + float x = wx; float y = wy; if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) @@ -1805,6 +1872,12 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) } else if(s_Operation == OP_MOVEUV) { + int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3); + + m_QuadTracker.BeginQuadPointPropTrack(pLayer, m_vSelectedQuads, SelectedPoints); + m_QuadTracker.AddQuadPointPropTrack(EQuadPointProp::PROP_TEX_U); + m_QuadTracker.AddQuadPointPropTrack(EQuadPointProp::PROP_TEX_V); + for(int m = 0; m < 4; m++) { if(IsQuadPointSelected(QuadIndex, m)) @@ -1852,6 +1925,15 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) SelectQuadPoint(QuadIndex, V); } + if(s_Operation == OP_MOVEPOINT) + { + m_QuadTracker.EndQuadTrack(); + } + else if(s_Operation == OP_MOVEUV) + { + m_QuadTracker.EndQuadPointPropTrackAll(); + } + UI()->DisableMouseLock(); UI()->SetActiveItem(nullptr); } @@ -2081,6 +2163,7 @@ void CEditor::DoQuadKnife(int QuadIndex) pResult->m_aPoints[4].y = ((pResult->m_aPoints[0].y + pResult->m_aPoints[3].y) / 2 + (pResult->m_aPoints[1].y + pResult->m_aPoints[2].y) / 2) / 2; m_QuadKnifeCount = 0; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0])); } // Render @@ -2677,6 +2760,8 @@ void CEditor::DoMapEditor(CUIRect View) std::shared_ptr pBrush = m_pBrush->IsEmpty() ? nullptr : m_pBrush->m_vpLayers[BrushIndex]; apEditLayers[k]->FillSelection(m_pBrush->IsEmpty(), pBrush, r); } + std::shared_ptr Action = std::make_shared(this, m_SelectedGroup); + m_EditorHistory.RecordAction(Action); } else { @@ -2707,6 +2792,7 @@ void CEditor::DoMapEditor(CUIRect View) size_t BrushIndex = k; if(m_pBrush->m_vpLayers.size() != NumEditLayers) BrushIndex = 0; + if(apEditLayers[k]->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type) apEditLayers[k]->BrushPlace(m_pBrush->m_vpLayers[BrushIndex], wx, wy); } @@ -2785,9 +2871,9 @@ void CEditor::DoMapEditor(CUIRect View) for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) { for(int v = 0; v < 4; v++) - DoQuadPoint(&pLayer->m_vQuads[i], i, v); + DoQuadPoint(pLayer, &pLayer->m_vQuads[i], i, v); - DoQuad(&pLayer->m_vQuads[i], i); + DoQuad(pLayer, &pLayer->m_vQuads[i], i); } Graphics()->QuadsEnd(); } @@ -2883,9 +2969,9 @@ void CEditor::DoMapEditor(CUIRect View) } } - // do panning if(UI()->CheckActiveItem(s_pEditorID)) { + // do panning if(s_Operation == OP_PAN_WORLD) MapView()->OffsetWorld(-vec2(m_MouseDeltaX, m_MouseDeltaY) * m_MouseWScale); else if(s_Operation == OP_PAN_EDITOR) @@ -2894,6 +2980,14 @@ void CEditor::DoMapEditor(CUIRect View) // release mouse if(!UI()->MouseButton(0)) { + if(s_Operation == OP_BRUSH_DRAW) + { + std::shared_ptr Action = std::make_shared(this, m_SelectedGroup); + + if(!Action->IsEmpty()) // Avoid recording tile draw action when placing quads only + m_EditorHistory.RecordAction(Action); + } + s_Operation = OP_NONE; UI()->SetActiveItem(nullptr); } @@ -3009,238 +3103,6 @@ void CEditor::SetHotQuadPoint(const std::shared_ptr &pLayer) UI()->SetHotItem(pMinPoint); } -int CEditor::DoProperties(CUIRect *pToolBox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) -{ - int Change = -1; - - for(int i = 0; pProps[i].m_pName; i++) - { - CUIRect Slot; - pToolBox->HSplitTop(13.0f, &Slot, pToolBox); - CUIRect Label, Shifter; - Slot.VSplitMid(&Label, &Shifter); - Shifter.HMargin(1.0f, &Shifter); - UI()->DoLabel(&Label, pProps[i].m_pName, 10.0f, TEXTALIGN_ML); - - if(pProps[i].m_Type == PROPTYPE_INT_STEP) - { - CUIRect Inc, Dec; - char aBuf[64]; - - Shifter.VSplitRight(10.0f, &Shifter, &Inc); - Shifter.VSplitLeft(10.0f, &Dec, &Shifter); - str_from_int(pProps[i].m_Value, aBuf); - int NewValue = UiDoValueSelector((char *)&pIDs[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0, &Color); - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue; - Change = i; - } - if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) - { - *pNewVal = pProps[i].m_Value - 1; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Increase")) - { - *pNewVal = pProps[i].m_Value + 1; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_BOOL) - { - CUIRect No, Yes; - Shifter.VSplitMid(&No, &Yes); - if(DoButton_ButtonDec(&pIDs[i], "No", !pProps[i].m_Value, &No, 0, "")) - { - *pNewVal = 0; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 1, "Yes", pProps[i].m_Value, &Yes, 0, "")) - { - *pNewVal = 1; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_INT_SCROLL) - { - int NewValue = UiDoValueSelector(&pIDs[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text."); - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_ANGLE_SCROLL) - { - CUIRect Inc, Dec; - Shifter.VSplitRight(10.0f, &Shifter, &Inc); - Shifter.VSplitLeft(10.0f, &Dec, &Shifter); - const bool Shift = Input()->ShiftIsPressed(); - int Step = Shift ? 1 : 45; - int Value = pProps[i].m_Value; - - int NewValue = UiDoValueSelector(&pIDs[i], &Shifter, "", Value, pProps[i].m_Min, pProps[i].m_Max, Shift ? 1 : 45, Shift ? 1.0f : 10.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0); - if(DoButton_ButtonDec(&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) - { - NewValue = (std::ceil((pProps[i].m_Value / (float)Step)) - 1) * Step; - if(NewValue < 0) - NewValue += 360; - } - if(DoButton_ButtonInc(&pIDs[i] + 2, nullptr, 0, &Inc, 0, "Increase")) - NewValue = (pProps[i].m_Value + Step) / Step * Step; - - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue % 360; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_COLOR) - { - const auto &&SetColor = [&](ColorRGBA NewColor) { - const int NewValue = NewColor.PackAlphaLast(); - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue; - Change = i; - } - }; - DoColorPickerButton(&pIDs[i], &Shifter, ColorRGBA::UnpackAlphaLast(pProps[i].m_Value), SetColor); - } - else if(pProps[i].m_Type == PROPTYPE_IMAGE) - { - const char *pName; - if(pProps[i].m_Value < 0) - pName = "None"; - else - pName = m_Map.m_vpImages[pProps[i].m_Value]->m_aName; - - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectImageInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); - - int r = PopupSelectImageResult(); - if(r >= -1) - { - *pNewVal = r; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_SHIFT) - { - CUIRect Left, Right, Up, Down; - Shifter.VSplitMid(&Left, &Up, 2.0f); - Left.VSplitLeft(10.0f, &Left, &Shifter); - Shifter.VSplitRight(10.0f, &Shifter, &Right); - Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), 0, 0.0f); - UI()->DoLabel(&Shifter, "X", 10.0f, TEXTALIGN_MC); - Up.VSplitLeft(10.0f, &Up, &Shifter); - Shifter.VSplitRight(10.0f, &Shifter, &Down); - Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), 0, 0.0f); - UI()->DoLabel(&Shifter, "Y", 10.0f, TEXTALIGN_MC); - if(DoButton_ButtonDec(&pIDs[i], "-", 0, &Left, 0, "Left")) - { - *pNewVal = DIRECTION_LEFT; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 3, "+", 0, &Right, 0, "Right")) - { - *pNewVal = DIRECTION_RIGHT; - Change = i; - } - if(DoButton_ButtonDec(((char *)&pIDs[i]) + 1, "-", 0, &Up, 0, "Up")) - { - *pNewVal = DIRECTION_UP; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, "+", 0, &Down, 0, "Down")) - { - *pNewVal = DIRECTION_DOWN; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_SOUND) - { - const char *pName; - if(pProps[i].m_Value < 0) - pName = "None"; - else - pName = m_Map.m_vpSounds[pProps[i].m_Value]->m_aName; - - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectSoundInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); - - int r = PopupSelectSoundResult(); - if(r >= -1) - { - *pNewVal = r; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_AUTOMAPPER) - { - const char *pName; - if(pProps[i].m_Value < 0 || pProps[i].m_Min < 0 || pProps[i].m_Min >= (int)m_Map.m_vpImages.size()) - pName = "None"; - else - pName = m_Map.m_vpImages[pProps[i].m_Min]->m_AutoMapper.GetConfigName(pProps[i].m_Value); - - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectConfigAutoMapInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); - - int r = PopupSelectConfigAutoMapResult(); - if(r >= -1) - { - *pNewVal = r; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_ENVELOPE) - { - CUIRect Inc, Dec; - char aBuf[8]; - int CurValue = pProps[i].m_Value; - - Shifter.VSplitRight(10.0f, &Shifter, &Inc); - Shifter.VSplitLeft(10.0f, &Dec, &Shifter); - - if(CurValue <= 0) - str_copy(aBuf, "None:"); - else if(m_Map.m_vpEnvelopes[CurValue - 1]->m_aName[0]) - { - str_format(aBuf, sizeof(aBuf), "%s:", m_Map.m_vpEnvelopes[CurValue - 1]->m_aName); - if(!str_endswith(aBuf, ":")) - { - aBuf[sizeof(aBuf) - 2] = ':'; - aBuf[sizeof(aBuf) - 1] = '\0'; - } - } - else - aBuf[0] = '\0'; - - int NewVal = UiDoValueSelector((char *)&pIDs[i], &Shifter, aBuf, CurValue, 0, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Set Envelope", false, false, IGraphics::CORNER_NONE); - if(NewVal != CurValue) - { - *pNewVal = NewVal; - Change = i; - } - - if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Previous Envelope")) - { - *pNewVal = pProps[i].m_Value - 1; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Next Envelope")) - { - *pNewVal = pProps[i].m_Value + 1; - Change = i; - } - } - } - - return Change; -} - void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRGBA Color, const std::function &SetColor) { CUIRect ColorRect; @@ -3261,6 +3123,7 @@ void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRG std::optional ParsedColor = color_parse(pClipboard); if(ParsedColor) { + m_ColorPickerPopupContext.m_State = EEditState::ONE_GO; SetColor(ParsedColor.value()); } } @@ -3292,6 +3155,13 @@ void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRG else { m_pColorPickerPopupActiveID = nullptr; + if(m_ColorPickerPopupContext.m_State == EEditState::EDITING) + { + ColorRGBA c = color_cast(m_ColorPickerPopupContext.m_HsvaColor); + m_ColorPickerPopupContext.m_State = EEditState::END; + SetColor(c); + m_ColorPickerPopupContext.m_State = EEditState::NONE; + } } } @@ -3319,6 +3189,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) OP_GROUP_DRAG }; static int s_Operation = OP_NONE; + static int s_PreviousOperation = OP_NONE; static const void *s_pDraggedButton = 0; static float s_InitialMouseY = 0; static float s_InitialCutHeight = 0; @@ -3332,6 +3203,14 @@ void CEditor::RenderLayers(CUIRect LayersBox) bool AnyButtonActive = false; std::vector vButtonsPerGroup; + auto SetOperation = [](int Operation) { + if(Operation != s_Operation) + { + s_PreviousOperation = s_Operation; + s_Operation = Operation; + } + }; + vButtonsPerGroup.reserve(m_Map.m_vpGroups.size()); for(const std::shared_ptr &pGroup : m_Map.m_vpGroups) { @@ -3339,7 +3218,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } if(!UI()->CheckActiveItem(s_pDraggedButton)) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG) { @@ -3437,14 +3316,14 @@ void CEditor::RenderLayers(CUIRect LayersBox) { s_InitialMouseY = UI()->MouseY(); s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y; - s_Operation = OP_CLICK; + SetOperation(OP_CLICK); if(g != m_SelectedGroup) SelectLayer(0, g); } if(Abrupted) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(s_Operation == OP_CLICK) { @@ -3477,7 +3356,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(!m_Map.m_vpGroups[g]->m_vpLayers.empty() && Input()->MouseDoubleClick()) m_Map.m_vpGroups[g]->m_Collapse ^= 1; - s_Operation = OP_NONE; + SetOperation(OP_NONE); } if(s_Operation == OP_GROUP_DRAG && Clicked) @@ -3485,7 +3364,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else if(s_pDraggedButton == &m_Map.m_vpGroups[g]) { - s_Operation = OP_NONE; + SetOperation(OP_NONE); } } @@ -3589,7 +3468,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) { s_InitialMouseY = UI()->MouseY(); s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y; - s_Operation = OP_CLICK; + SetOperation(OP_CLICK); if(!Input()->ShiftIsPressed() && !IsLayerSelected) { @@ -3598,7 +3477,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } if(Abrupted) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(s_Operation == OP_CLICK) { @@ -3649,23 +3528,33 @@ void CEditor::RenderLayers(CUIRect LayersBox) bool AllTile = true; for(size_t j = 0; AllTile && j < m_vSelectedLayers.size(); j++) { - if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[m_vSelectedLayers[j]]->m_Type == LAYERTYPE_TILES) + int LayerIndex = m_vSelectedLayers[j]; + if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES) + { s_LayerPopupContext.m_vpLayers.push_back(std::static_pointer_cast(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[m_vSelectedLayers[j]])); + s_LayerPopupContext.m_vLayerIndices.push_back(LayerIndex); + } else AllTile = false; } - // Don't allow editing if all selected layers are tile layers + // Don't allow editing if all selected layers are not tile layers if(!AllTile) + { s_LayerPopupContext.m_vpLayers.clear(); + s_LayerPopupContext.m_vLayerIndices.clear(); + } } else + { s_LayerPopupContext.m_vpLayers.clear(); + s_LayerPopupContext.m_vLayerIndices.clear(); + } UI()->DoPopupMenu(&s_LayerPopupContext, UI()->MouseX(), UI()->MouseY(), 120, 270, &s_LayerPopupContext, PopupLayer); } - s_Operation = OP_NONE; + SetOperation(OP_NONE); } if(s_Operation == OP_LAYER_DRAG && Clicked) @@ -3675,7 +3564,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else if(s_pDraggedButton == m_Map.m_vpGroups[g]->m_vpLayers[i].get()) { - s_Operation = OP_NONE; + SetOperation(OP_NONE); } } @@ -3755,16 +3644,28 @@ void CEditor::RenderLayers(CUIRect LayersBox) auto InsertPosition = std::find(m_Map.m_vpGroups.begin(), m_Map.m_vpGroups.end(), pNextGroup); m_Map.m_vpGroups.insert(InsertPosition, pSelectedGroup); - m_SelectedGroup = InsertPosition - m_Map.m_vpGroups.begin(); + auto Pos = std::find(m_Map.m_vpGroups.begin(), m_Map.m_vpGroups.end(), pSelectedGroup); + m_SelectedGroup = Pos - m_Map.m_vpGroups.begin(); + m_Map.OnModify(); } + static int s_InitialGroupIndex; + static std::vector s_vInitialLayerIndices; + if(MoveLayers || MoveGroup) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(StartDragLayer) - s_Operation = OP_LAYER_DRAG; + { + SetOperation(OP_LAYER_DRAG); + s_InitialGroupIndex = m_SelectedGroup; + s_vInitialLayerIndices = std::vector(m_vSelectedLayers); + } if(StartDragGroup) - s_Operation = OP_GROUP_DRAG; + { + s_InitialGroupIndex = m_SelectedGroup; + SetOperation(OP_GROUP_DRAG); + } if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG) { @@ -3847,13 +3748,41 @@ void CEditor::RenderLayers(CUIRect LayersBox) { m_Map.NewGroup(); m_SelectedGroup = m_Map.m_vpGroups.size() - 1; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, false)); } } s_ScrollRegion.End(); if(!AnyButtonActive) - s_Operation = OP_NONE; + SetOperation(OP_NONE); + + if(s_Operation == OP_NONE) + { + if(s_PreviousOperation == OP_GROUP_DRAG) + { + s_PreviousOperation = OP_NONE; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, EGroupProp::PROP_ORDER, s_InitialGroupIndex, m_SelectedGroup)); + } + else if(s_PreviousOperation == OP_LAYER_DRAG) + { + if(s_InitialGroupIndex != m_SelectedGroup) + { + m_EditorHistory.RecordAction(std::make_shared(this, s_InitialGroupIndex, s_vInitialLayerIndices, m_SelectedGroup, m_vSelectedLayers)); + } + else + { + std::vector> vpActions; + for(int k = 0; k < (int)m_vSelectedLayers.size(); k++) + { + int LayerIndex = m_vSelectedLayers[k]; + vpActions.push_back(std::make_shared(this, m_SelectedGroup, LayerIndex, ELayerProp::PROP_ORDER, s_vInitialLayerIndices[k], LayerIndex)); + } + m_EditorHistory.RecordAction(std::make_shared(CEditorActionBulk(this, vpActions))); + } + s_PreviousOperation = OP_NONE; + } + } } bool CEditor::SelectLayerByTile() @@ -4172,7 +4101,7 @@ void CEditor::SelectGameLayer() } } -void CEditor::SortImages() +std::vector CEditor::SortImages() { static const auto &&s_ImageNameComparator = [](const std::shared_ptr &pLhs, const std::shared_ptr &pRhs) { return str_comp(pLhs->m_aName, pRhs->m_aName) < 0; @@ -4199,7 +4128,11 @@ void CEditor::SortImages() if(*pIndex >= 0) *pIndex = vSortedIndex[*pIndex]; }); + + return vSortedIndex; } + + return std::vector(); } void CEditor::RenderImagesList(CUIRect ToolBox) @@ -4320,7 +4253,7 @@ void CEditor::RenderImagesList(CUIRect ToolBox) if(Result == 2) { const std::shared_ptr pImg = m_Map.m_vpImages[m_SelectedImage]; - const int Height = !pImg->m_External && IsVanillaImage(pImg->m_aName) ? 107 : pImg->m_External ? 73 : 90; + const int Height = !pImg->m_External && IsVanillaImage(pImg->m_aName) ? 107 : (pImg->m_External ? 73 : 90); static SPopupMenuId s_PopupImageId; UI()->DoPopupMenu(&s_PopupImageId, UI()->MouseX(), UI()->MouseY(), 140, Height, this, PopupImage); } @@ -5300,6 +5233,14 @@ void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect) m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS ? EXTRAEDITOR_NONE : EXTRAEDITOR_SERVER_SETTINGS; } + View.VSplitRight(10.0f, &View, nullptr); + View.VSplitRight(100.0f, &View, &Button); + static int s_HistoryButton = 0; + if(DoButton_Editor(&s_HistoryButton, "History", ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_HISTORY, &Button, 0, "Toggles the editor history view.") == 1) + { + m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_HISTORY ? EXTRAEDITOR_NONE : EXTRAEDITOR_HISTORY; + } + View.VSplitRight(10.0f, pTooltipRect, nullptr); } @@ -5652,18 +5593,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(m_SelectedEnvelope >= 0 && m_SelectedEnvelope < (int)m_Map.m_vpEnvelopes.size()) pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; - enum - { - OP_NONE, - OP_SELECT, - OP_DRAG_POINT, - OP_DRAG_POINT_X, - OP_DRAG_POINT_Y, - OP_CONTEXT_MENU, - OP_BOX_SELECT, - OP_SCALE - }; - static int s_Operation = OP_NONE; + static EEnvelopeEditorOp s_Operation = EEnvelopeEditorOp::OP_NONE; static std::vector s_vAccurateDragValuesX = {}; static std::vector s_vAccurateDragValuesY = {}; static float s_MouseXStart = 0.0f; @@ -5688,6 +5618,23 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) CUIRect Button; std::shared_ptr pNewEnv = nullptr; + // redo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_EnvelopeEditorHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo last action", IGraphics::CORNER_R, 11.0f) == 1) + { + m_EnvelopeEditorHistory.Redo(); + } + + // undo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(10.0f, &ToolBar, nullptr); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_EnvelopeEditorHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo last action", IGraphics::CORNER_L, 11.0f) == 1) + { + m_EnvelopeEditorHistory.Undo(); + } + ToolBar.VSplitRight(50.0f, &ToolBar, &Button); static int s_NewSoundButton = 0; if(DoButton_Editor(&s_NewSoundButton, "Sound+", 0, &Button, 0, "Creates a new sound envelope")) @@ -5722,6 +5669,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_DeleteButton = 0; if(DoButton_Editor(&s_DeleteButton, "✗", 0, &Button, 0, "Delete this envelope")) { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope)); m_Map.DeleteEnvelope(m_SelectedEnvelope); if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size()) m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1; @@ -5734,6 +5682,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_MoveRightButton = 0; if(DoButton_Ex(&s_MoveRightButton, "→", 0, &Button, 0, "Move this envelope to the right", IGraphics::CORNER_R)) { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope, CEditorActionEnvelopeEdit::EEditType::ORDER, m_SelectedEnvelope, m_SelectedEnvelope + 1)); m_Map.SwapEnvelopes(m_SelectedEnvelope, m_SelectedEnvelope + 1); m_SelectedEnvelope = clamp(m_SelectedEnvelope + 1, 0, m_Map.m_vpEnvelopes.size() - 1); pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; @@ -5744,6 +5693,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_MoveLeftButton = 0; if(DoButton_Ex(&s_MoveLeftButton, "←", 0, &Button, 0, "Move this envelope to the left", IGraphics::CORNER_L)) { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope, CEditorActionEnvelopeEdit::EEditType::ORDER, m_SelectedEnvelope, m_SelectedEnvelope - 1)); m_Map.SwapEnvelopes(m_SelectedEnvelope - 1, m_SelectedEnvelope); m_SelectedEnvelope = clamp(m_SelectedEnvelope - 1, 0, m_Map.m_vpEnvelopes.size() - 1); pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; @@ -5794,6 +5744,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) pNewEnv->AddPoint(0, 0); pNewEnv->AddPoint(1000, 0); } + + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, pNewEnv)); } CUIRect Shifter, Inc, Dec; @@ -5806,13 +5758,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ColorRGBA EnvColor = ColorRGBA(1, 1, 1, 0.5f); if(!m_Map.m_vpEnvelopes.empty()) { - EnvColor = IsEnvelopeUsed(m_SelectedEnvelope) ? - ColorRGBA(1, 0.7f, 0.7f, 0.5f) : - ColorRGBA(0.7f, 1, 0.7f, 0.5f); + EnvColor = IsEnvelopeUsed(m_SelectedEnvelope) ? ColorRGBA(1, 0.7f, 0.7f, 0.5f) : ColorRGBA(0.7f, 1, 0.7f, 0.5f); } static int s_EnvelopeSelector = 0; - int NewValue = UiDoValueSelector(&s_EnvelopeSelector, &Shifter, aBuf, m_SelectedEnvelope + 1, 1, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Select Envelope", false, false, IGraphics::CORNER_NONE, &EnvColor, false); + auto NewValueRes = UiDoValueSelector(&s_EnvelopeSelector, &Shifter, aBuf, m_SelectedEnvelope + 1, 1, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Select Envelope", false, false, IGraphics::CORNER_NONE, &EnvColor, false); + int NewValue = NewValueRes.m_Value; if(NewValue - 1 != m_SelectedEnvelope) { m_SelectedEnvelope = NewValue - 1; @@ -5921,7 +5872,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ToolBar.VSplitLeft(12.0f, &Button, &ToolBar); static int s_SyncButton; if(DoButton_Editor(&s_SyncButton, pEnvelope->m_Synchronized ? "X" : "", 0, &Button, 0, "Synchronize envelope animation to game time (restarts when you touch the start line)")) + { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope, CEditorActionEnvelopeEdit::EEditType::SYNC, pEnvelope->m_Synchronized, !pEnvelope->m_Synchronized)); pEnvelope->m_Synchronized = !pEnvelope->m_Synchronized; + } ToolBar.VSplitLeft(4.0f, nullptr, &ToolBar); ToolBar.VSplitLeft(40.0f, &Button, &ToolBar); @@ -5931,7 +5885,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { UI()->SetHotItem(&s_EnvelopeEditorID); - if(s_Operation == OP_NONE && (UI()->MouseButton(2) || (UI()->MouseButton(0) && Input()->ModifierIsPressed()))) + if(s_Operation == EEnvelopeEditorOp::OP_NONE && (UI()->MouseButton(2) || (UI()->MouseButton(0) && Input()->ModifierIsPressed()))) { m_OffsetEnvelopeX += UI()->MouseDeltaX() / Graphics()->ScreenWidth() * UI()->Screen()->w / View.w; m_OffsetEnvelopeY -= UI()->MouseDeltaY() / Graphics()->ScreenHeight() * UI()->Screen()->h / View.h; @@ -5986,19 +5940,17 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } if(!TimeFound) - pEnvelope->AddPoint(FixedTime, - f2fx(Channels.r), f2fx(Channels.g), - f2fx(Channels.b), f2fx(Channels.a)); + m_EnvelopeEditorHistory.Execute(std::make_shared(this, m_SelectedEnvelope, FixedTime, Channels)); if(FixedTime < 0) RemoveTimeOffsetEnvelope(pEnvelope); m_Map.OnModify(); } - else if(s_Operation != OP_BOX_SELECT && !Input()->ModifierIsPressed()) + else if(s_Operation != EEnvelopeEditorOp::OP_BOX_SELECT && !Input()->ModifierIsPressed()) { static int s_BoxSelectID = 0; UI()->SetActiveItem(&s_BoxSelectID); - s_Operation = OP_BOX_SELECT; + s_Operation = EEnvelopeEditorOp::OP_BOX_SELECT; s_MouseXStart = UI()->MouseX(); s_MouseYStart = UI()->MouseY(); } @@ -6214,7 +6166,13 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(CurveButton.x >= View.x) { if(DoButton_Editor(pID, pTypeName, 0, &CurveButton, 0, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)")) + { + int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype; pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES; + + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, + m_SelectedEnvelope, i, 0, CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, PrevCurve, pEnvelope->m_vPoints[i].m_Curvetype)); + } } } } @@ -6275,8 +6233,16 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->DoPopupMenu(&s_PopupEnvPointId, UI()->MouseX(), UI()->MouseY(), 150, 56 + (pEnvelope->GetChannels() == 4 ? 16.0f : 0.0f), this, PopupEnvPoint); }; - if(s_Operation == OP_NONE) + if(s_Operation == EEnvelopeEditorOp::OP_NONE) + { SetHotEnvelopePoint(View, pEnvelope, s_ActiveChannels); + if(!UI()->MouseButton(0)) + m_EnvOpTracker.Stop(false); + } + else + { + m_EnvOpTracker.Begin(s_Operation); + } UI()->ClipEnable(&View); Graphics()->TextureClear(); @@ -6286,7 +6252,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(!(s_ActiveChannels & (1 << c))) continue; - for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++) + for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++) { // point handle { @@ -6316,27 +6282,27 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { m_ShowEnvelopePreview = SHOWENV_SELECTED; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { float dx = s_MouseXStart - UI()->MouseX(); float dy = s_MouseYStart - UI()->MouseY(); if(dx * dx + dy * dy > 20.0f) { - s_Operation = OP_DRAG_POINT; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT; if(!IsEnvPointSelected(i, c)) SelectEnvPoint(i, c); } } - if(s_Operation == OP_DRAG_POINT || s_Operation == OP_DRAG_POINT_X || s_Operation == OP_DRAG_POINT_Y) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y) { if(Input()->ShiftIsPressed()) { - if(s_Operation == OP_DRAG_POINT || s_Operation == OP_DRAG_POINT_Y) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y) { - s_Operation = OP_DRAG_POINT_X; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_X; s_vAccurateDragValuesX.clear(); for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints) s_vAccurateDragValuesX.push_back(pEnvelope->m_vPoints[SelectedIndex].m_Time); @@ -6385,9 +6351,9 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - if(s_Operation == OP_DRAG_POINT || s_Operation == OP_DRAG_POINT_X) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X) { - s_Operation = OP_DRAG_POINT_Y; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_Y; s_vAccurateDragValuesY.clear(); for(auto [SelectedIndex, SelectedChannel] : m_vSelectedEnvelopePoints) s_vAccurateDragValuesY.push_back(pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]); @@ -6411,7 +6377,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) { @@ -6426,7 +6392,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->DoPopupMenu(&s_PopupEnvPointMultiId, UI()->MouseX(), UI()->MouseY(), 80, 22, this, PopupEnvPointMulti); } UI()->SetActiveItem(nullptr); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; } } else if(!UI()->MouseButton(0)) @@ -6434,7 +6400,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { if(Input()->ShiftIsPressed()) ToggleEnvPoint(i, c); @@ -6442,7 +6408,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) SelectEnvPoint(i, c); } - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; m_Map.OnModify(); } @@ -6453,7 +6419,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { UI()->SetActiveItem(pID); - s_Operation = OP_SELECT; + s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; s_MouseXStart = UI()->MouseX(); @@ -6463,12 +6429,11 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { if(Input()->ShiftIsPressed()) { - pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); - m_Map.OnModify(); + m_EnvelopeEditorHistory.Execute(std::make_shared(this, m_SelectedEnvelope, i)); } else { - s_Operation = OP_CONTEXT_MENU; + s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; if(!IsEnvPointSelected(i, c)) SelectEnvPoint(i, c); UI()->SetActiveItem(pID); @@ -6488,6 +6453,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } // tangent handles for bezier curves + if(i >= 0 && i < (int)pEnvelope->m_vPoints.size()) { // Out-Tangent handle if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER) @@ -6522,14 +6488,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { m_ShowEnvelopePreview = SHOWENV_SELECTED; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { float dx = s_MouseXStart - UI()->MouseX(); float dy = s_MouseYStart - UI()->MouseY(); if(dx * dx + dy * dy > 20.0f) { - s_Operation = OP_DRAG_POINT; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT; s_vAccurateDragValuesX = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c])}; s_vAccurateDragValuesY = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c])}; @@ -6539,7 +6505,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(s_Operation == OP_DRAG_POINT) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT) { float DeltaX = ScreenToEnvelopeDX(View, UI()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); float DeltaY = ScreenToEnvelopeDY(View, UI()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); @@ -6554,7 +6520,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_vAccurateDragValuesX[0] = clamp(s_vAccurateDragValuesX[0], 0, f2fxt(ScreenToEnvelopeX(View, View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time); } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) { @@ -6571,10 +6537,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) SelectTangentOutPoint(i, c); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; m_Map.OnModify(); } @@ -6585,7 +6551,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { UI()->SetActiveItem(pID); - s_Operation = OP_SELECT; + s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; s_MouseXStart = UI()->MouseX(); @@ -6602,7 +6568,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - s_Operation = OP_CONTEXT_MENU; + s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; SelectTangentOutPoint(i, c); UI()->SetActiveItem(pID); } @@ -6654,14 +6620,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { m_ShowEnvelopePreview = SHOWENV_SELECTED; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { float dx = s_MouseXStart - UI()->MouseX(); float dy = s_MouseYStart - UI()->MouseY(); if(dx * dx + dy * dy > 20.0f) { - s_Operation = OP_DRAG_POINT; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT; s_vAccurateDragValuesX = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c])}; s_vAccurateDragValuesY = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c])}; @@ -6671,7 +6637,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(s_Operation == OP_DRAG_POINT) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT) { float DeltaX = ScreenToEnvelopeDX(View, UI()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); float DeltaY = ScreenToEnvelopeDY(View, UI()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); @@ -6686,7 +6652,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_vAccurateDragValuesX[0] = clamp(s_vAccurateDragValuesX[0], f2fxt(ScreenToEnvelopeX(View, View.x)) - pEnvelope->m_vPoints[i].m_Time, 0); } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) { @@ -6703,10 +6669,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) SelectTangentInPoint(i, c); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; m_Map.OnModify(); } @@ -6717,7 +6683,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { UI()->SetActiveItem(pID); - s_Operation = OP_SELECT; + s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; s_MouseXStart = UI()->MouseX(); @@ -6734,7 +6700,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - s_Operation = OP_CONTEXT_MENU; + s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; SelectTangentInPoint(i, c); UI()->SetActiveItem(pID); } @@ -6766,9 +6732,9 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static float s_MidpointY = 0.0f; static std::vector s_vInitialPositionsX; static std::vector s_vInitialPositionsY; - if(s_Operation == OP_NONE && Input()->KeyIsPressed(KEY_S) && !Input()->ModifierIsPressed() && !m_vSelectedEnvelopePoints.empty()) + if(s_Operation == EEnvelopeEditorOp::OP_NONE && Input()->KeyIsPressed(KEY_S) && !Input()->ModifierIsPressed() && !m_vSelectedEnvelopePoints.empty()) { - s_Operation = OP_SCALE; + s_Operation = EEnvelopeEditorOp::OP_SCALE; s_ScaleFactorX = 1.0f; s_ScaleFactorY = 1.0f; auto [FirstPointIndex, FirstPointChannel] = m_vSelectedEnvelopePoints.front(); @@ -6798,7 +6764,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_MidpointY = (MaximumY - MinimumY) / 2.0f + MinimumY; } - if(s_Operation == OP_SCALE) + if(s_Operation == EEnvelopeEditorOp::OP_SCALE) { str_copy(m_aTooltip, "Press shift to scale the time. Press alt to scale along midpoint. Press ctrl to be more precise."); @@ -6873,7 +6839,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; + m_EnvOpTracker.Stop(false); } else if(UI()->MouseButton(1) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) { @@ -6888,12 +6855,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(s_vInitialPositionsY[k]); } RemoveTimeOffsetEnvelope(pEnvelope); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; } } // handle box selection - if(s_Operation == OP_BOX_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_BOX_SELECT) { IGraphics::CLineItem aLines[4] = { {s_MouseXStart, s_MouseYStart, UI()->MouseX(), s_MouseYStart}, @@ -6908,7 +6875,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(!UI()->MouseButton(0)) { - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; UI()->SetActiveItem(nullptr); float TimeStart = ScreenToEnvelopeX(View, s_MouseXStart); @@ -6955,7 +6922,7 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd static CListBox s_ListBox; s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); - const bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); const bool CurrentInputValid = ValidateServerSetting(m_SettingsCommandInput.GetString()); CUIRect ToolBar, Button, Label, List, DragBar; @@ -6974,6 +6941,8 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd static int s_DeleteButton = 0; if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, GotSelection ? 0 : -1, &Button, 0, "[Delete] Delete the selected command from the command list.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size()) s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; @@ -6989,6 +6958,8 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd static int s_DownButton = 0; if(DoButton_FontIcon(&s_DownButton, FONT_ICON_SORT_DOWN, CanMoveDown ? 0 : -1, &Button, 0, "[Alt+Down] Move the selected command down.", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_DOWN, &s_CommandSelectedIndex, s_CommandSelectedIndex)); + std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex + 1]); s_CommandSelectedIndex++; m_Map.OnModify(); @@ -7002,12 +6973,33 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd static int s_UpButton = 0; if(DoButton_FontIcon(&s_UpButton, FONT_ICON_SORT_UP, CanMoveUp ? 0 : -1, &Button, 0, "[Alt+Up] Move the selected command up.", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_UP, &s_CommandSelectedIndex, s_CommandSelectedIndex)); + std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex - 1]); s_CommandSelectedIndex--; m_Map.OnModify(); s_ListBox.ScrollToSelected(); } + // redo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_ServerSettingsHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo command edit", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) + { + m_ServerSettingsHistory.Redo(); + } + + // undo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_ServerSettingsHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo command edit", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) + { + m_ServerSettingsHistory.Undo(); + } + + GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + // update button ToolBar.VSplitRight(25.0f, &ToolBar, &Button); const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()) != 0; @@ -7026,12 +7018,15 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd } if(Found) { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i; } else { - str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()); + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); } m_Map.OnModify(); s_ListBox.ScrollToSelected(); @@ -7060,6 +7055,7 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd { m_Map.m_vSettings.emplace_back(m_SettingsCommandInput.GetString()); s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::ADD, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); m_Map.OnModify(); } s_ListBox.ScrollToSelected(); @@ -7097,6 +7093,157 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd } } +void CEditor::RenderEditorHistory(CUIRect View) +{ + enum EHistoryType + { + EDITOR_HISTORY, + ENVELOPE_HISTORY, + SERVER_SETTINGS_HISTORY + }; + + static EHistoryType s_HistoryType = EDITOR_HISTORY; + static int s_ActionSelectedIndex = 0; + static CListBox s_ListBox; + s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); + + const bool GotSelection = s_ListBox.Active() && s_ActionSelectedIndex >= 0 && (size_t)s_ActionSelectedIndex < m_Map.m_vSettings.size(); + + CUIRect ToolBar, Button, Label, List, DragBar; + View.HSplitTop(22.0f, &DragBar, nullptr); + DragBar.y -= 2.0f; + DragBar.w += 2.0f; + DragBar.h += 4.0f; + RenderExtraEditorDragBar(View, DragBar); + View.HSplitTop(20.0f, &ToolBar, &View); + View.HSplitTop(2.0f, nullptr, &List); + ToolBar.HMargin(2.0f, &ToolBar); + + CUIRect TypeButtons, HistoryTypeButton; + const int HistoryTypeBtnSize = 70.0f; + ToolBar.VSplitLeft(3 * HistoryTypeBtnSize, &TypeButtons, &Label); + + // history type buttons + { + TypeButtons.VSplitLeft(HistoryTypeBtnSize, &HistoryTypeButton, &TypeButtons); + static int s_EditorHistoryButton = 0; + if(DoButton_Ex(&s_EditorHistoryButton, "Editor", s_HistoryType == EDITOR_HISTORY, &HistoryTypeButton, 0, "Show map editor history.", IGraphics::CORNER_L)) + { + s_HistoryType = EDITOR_HISTORY; + } + + TypeButtons.VSplitLeft(HistoryTypeBtnSize, &HistoryTypeButton, &TypeButtons); + static int s_EnvelopeEditorHistoryButton = 0; + if(DoButton_Ex(&s_EnvelopeEditorHistoryButton, "Envelope", s_HistoryType == ENVELOPE_HISTORY, &HistoryTypeButton, 0, "Show envelope editor history.", IGraphics::CORNER_NONE)) + { + s_HistoryType = ENVELOPE_HISTORY; + } + + TypeButtons.VSplitLeft(HistoryTypeBtnSize, &HistoryTypeButton, &TypeButtons); + static int s_ServerSettingsHistoryButton = 0; + if(DoButton_Ex(&s_ServerSettingsHistoryButton, "Settings", s_HistoryType == SERVER_SETTINGS_HISTORY, &HistoryTypeButton, 0, "Show server settings editor history.", IGraphics::CORNER_R)) + { + s_HistoryType = SERVER_SETTINGS_HISTORY; + } + } + + SLabelProperties InfoProps; + InfoProps.m_MaxWidth = ToolBar.w - 60.f; + InfoProps.m_EllipsisAtEnd = true; + Label.VSplitLeft(8.0f, nullptr, &Label); + UI()->DoLabel(&Label, "Editor history. Click on an action to undo all actions above.", 10.0f, TEXTALIGN_ML, InfoProps); + + CEditorHistory *pCurrentHistory; + if(s_HistoryType == EDITOR_HISTORY) + pCurrentHistory = &m_EditorHistory; + else if(s_HistoryType == ENVELOPE_HISTORY) + pCurrentHistory = &m_EnvelopeEditorHistory; + else if(s_HistoryType == SERVER_SETTINGS_HISTORY) + pCurrentHistory = &m_ServerSettingsHistory; + else + return; + + // delete button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + static int s_DeleteButton = 0; + if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, (!pCurrentHistory->m_vpUndoActions.empty() || !pCurrentHistory->m_vpRedoActions.empty()) ? 0 : -1, &Button, 0, "Clear the history.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) + { + pCurrentHistory->Clear(); + s_ActionSelectedIndex = 0; + } + + // actions list + int RedoSize = (int)pCurrentHistory->m_vpRedoActions.size(); + int UndoSize = (int)pCurrentHistory->m_vpUndoActions.size(); + s_ActionSelectedIndex = RedoSize; + s_ListBox.DoStart(15.0f, RedoSize + UndoSize, 1, 3, s_ActionSelectedIndex, &List); + + for(int i = 0; i < RedoSize; i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(&pCurrentHistory->m_vpRedoActions[i], s_ActionSelectedIndex >= 0 && s_ActionSelectedIndex == i); + if(!Item.m_Visible) + continue; + + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + TextRender()->TextColor({.5f, .5f, .5f}); + TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); + UI()->DoLabel(&Label, pCurrentHistory->m_vpRedoActions[i]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + + for(int i = 0; i < UndoSize; i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(&pCurrentHistory->m_vpUndoActions[UndoSize - i - 1], s_ActionSelectedIndex >= RedoSize && s_ActionSelectedIndex == (i + RedoSize)); + if(!Item.m_Visible) + continue; + + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + UI()->DoLabel(&Label, pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); + } + + { // Base action "Loaded map" that cannot be undone + static int s_BaseAction; + const CListboxItem Item = s_ListBox.DoNextItem(&s_BaseAction, s_ActionSelectedIndex == RedoSize + UndoSize); + if(Item.m_Visible) + { + Item.m_Rect.VMargin(5.0f, &Label); + + UI()->DoLabel(&Label, "Loaded map", 10.0f, TEXTALIGN_ML); + } + } + + const int NewSelected = s_ListBox.DoEnd(); + if(s_ActionSelectedIndex != NewSelected) + { + // Figure out if we should undo or redo some actions + // Undo everything until the selected index + if(NewSelected > s_ActionSelectedIndex) + { + for(int i = 0; i < (NewSelected - s_ActionSelectedIndex); i++) + { + pCurrentHistory->Undo(); + } + } + else + { + for(int i = 0; i < (s_ActionSelectedIndex - NewSelected); i++) + { + pCurrentHistory->Redo(); + } + } + s_ActionSelectedIndex = NewSelected; + } +} + void CEditor::RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar) { enum EDragOperation @@ -7250,6 +7397,12 @@ void CEditor::Render() if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr) { + // handle undo/redo hotkeys + if(Input()->KeyPress(KEY_Z) && Input()->ModifierIsPressed()) + UndoLastAction(); + if(Input()->KeyPress(KEY_Y) && Input()->ModifierIsPressed()) + RedoLastAction(); + // handle brush save/load hotkeys for(int i = KEY_1; i <= KEY_0; i++) { @@ -7416,6 +7569,10 @@ void CEditor::Render() { RenderServerSettingsEditor(ExtraEditor, s_ShowServerSettingsEditorLast); } + else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY) + { + RenderEditorHistory(ExtraEditor); + } s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS; } RenderStatusbar(StatusBar, &TooltipRect); @@ -7583,6 +7740,15 @@ void CEditor::Reset(bool CreateDefault) m_ResetZoomEnvelope = true; m_SettingsCommandInput.Clear(); + + m_EditorHistory.Clear(); + m_EnvelopeEditorHistory.Clear(); + m_ServerSettingsHistory.Clear(); + + m_QuadTracker.m_pEditor = this; + + m_EnvOpTracker.m_pEditor = this; + m_EnvOpTracker.Reset(); } int CEditor::GetTextureUsageFlag() @@ -7678,17 +7844,23 @@ void CEditor::PlaceBorderTiles() { std::shared_ptr pT = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_TILES)); - for(int i = 0; i < pT->m_Width * 2; ++i) - pT->m_pTiles[i].m_Index = 1; - for(int i = 0; i < pT->m_Width * pT->m_Height; ++i) { - if(i % pT->m_Width < 2 || i % pT->m_Width > pT->m_Width - 3) - pT->m_pTiles[i].m_Index = 1; + if(i % pT->m_Width < 2 || i % pT->m_Width > pT->m_Width - 3 || i < pT->m_Width * 2 || i > pT->m_Width * (pT->m_Height - 2)) + { + int x = i % pT->m_Width; + int y = i / pT->m_Width; + + CTile Current = pT->m_pTiles[i]; + Current.m_Index = 1; + pT->SetTile(x, y, Current); + } } - for(int i = (pT->m_Width * (pT->m_Height - 2)); i < pT->m_Width * pT->m_Height; ++i) - pT->m_pTiles[i].m_Index = 1; + int GameGroupIndex = std::find(m_Map.m_vpGroups.begin(), m_Map.m_vpGroups.end(), m_Map.m_pGameGroup) - m_Map.m_vpGroups.begin(); + m_EditorHistory.RecordAction(std::make_shared(this, GameGroupIndex), "Tool 'Make borders'"); + + m_Map.OnModify(); } void CEditor::HandleCursorMovement() @@ -8046,7 +8218,7 @@ bool CEditor::Load(const char *pFileName, int StorageType) return Result; } -bool CEditor::Append(const char *pFileName, int StorageType) +bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory) { CEditorMap NewMap; NewMap.m_pEditor = this; @@ -8058,6 +8230,12 @@ bool CEditor::Append(const char *pFileName, int StorageType) if(!NewMap.Load(pFileName, StorageType, std::move(ErrorHandler))) return false; + CEditorActionAppendMap::SPrevInfo Info{ + (int)m_Map.m_vpGroups.size(), + (int)m_Map.m_vpImages.size(), + (int)m_Map.m_vpSounds.size(), + (int)m_Map.m_vpEnvelopes.size()}; + static const auto &&s_ReplaceIndex = [](int ToReplace, int ReplaceWith) { return [ToReplace, ReplaceWith](int *pIndex) { if(*pIndex == ToReplace) @@ -8124,10 +8302,33 @@ bool CEditor::Append(const char *pFileName, int StorageType) } NewMap.m_vpGroups.clear(); - SortImages(); + auto IndexMap = SortImages(); + + if(!IgnoreHistory) + m_EditorHistory.RecordAction(std::make_shared(this, pFileName, Info, IndexMap)); // all done \o/ return true; } IEditor *CreateEditor() { return new CEditor; } + +void CEditor::UndoLastAction() +{ + if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS) + m_ServerSettingsHistory.Undo(); + else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES) + m_EnvelopeEditorHistory.Undo(); + else + m_EditorHistory.Undo(); +} + +void CEditor::RedoLastAction() +{ + if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS) + m_ServerSettingsHistory.Redo(); + else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES) + m_EnvelopeEditorHistory.Redo(); + else + m_EditorHistory.Redo(); +} diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 9dcd36b72..7eb0cfe20 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -30,6 +30,8 @@ #include #include "auto_map.h" +#include "editor_history.h" +#include "editor_trackers.h" #include "map_view.h" #include "smooth_value.h" @@ -452,7 +454,7 @@ public: bool Save(const char *pFilename) override; bool Load(const char *pFilename, int StorageType) override; bool HandleMapDrop(const char *pFilename, int StorageType) override; - bool Append(const char *pFilename, int StorageType); + bool Append(const char *pFilename, int StorageType, bool IgnoreHistory = false); void LoadCurrentMap(); void Render(); @@ -494,7 +496,10 @@ public: bool IsTangentInSelected() const; bool IsTangentOutSelected() const; bool IsTangentSelected() const; + std::pair EnvGetSelectedTimeAndValue() const; + template + SEditResult DoPropertiesWithState(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color = ColorRGBA(1, 1, 1, 0.5f)); int DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color = ColorRGBA(1, 1, 1, 0.5f)); CUI::SColorPickerPopupContext m_ColorPickerPopupContext; @@ -694,10 +699,11 @@ public: EXTRAEDITOR_NONE = -1, EXTRAEDITOR_ENVELOPES, EXTRAEDITOR_SERVER_SETTINGS, + EXTRAEDITOR_HISTORY, NUM_EXTRAEDITORS, }; EExtraEditor m_ActiveExtraEditor = EXTRAEDITOR_NONE; - float m_aExtraEditorSplits[NUM_EXTRAEDITORS] = {250.0f, 250.0f}; + float m_aExtraEditorSplits[NUM_EXTRAEDITORS] = {250.0f, 250.0f, 250.0f}; enum EShowEnvelope { @@ -752,7 +758,7 @@ public: CImageInfo m_TileartImageInfo; char m_aTileartFilename[IO_MAX_PATH_LENGTH]; - void AddTileart(); + void AddTileart(bool IgnoreHistory = false); void TileartCheckColors(); void PlaceBorderTiles(); @@ -779,7 +785,7 @@ public: void RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness); - int UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree = false, bool IsHex = false, int corners = IGraphics::CORNER_ALL, ColorRGBA *pColor = nullptr, bool ShowValue = true); + SEditResult UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree = false, bool IsHex = false, int corners = IGraphics::CORNER_ALL, ColorRGBA *pColor = nullptr, bool ShowValue = true); static CUI::EPopupMenuFunctionResult PopupMenuFile(void *pContext, CUIRect View, bool Active); static CUI::EPopupMenuFunctionResult PopupMenuTools(void *pContext, CUIRect View, bool Active); @@ -789,6 +795,7 @@ public: { CEditor *m_pEditor; std::vector> m_vpLayers; + std::vector m_vLayerIndices; CLayerTiles::SCommonPropState m_CommonPropState; }; static CUI::EPopupMenuFunctionResult PopupLayer(void *pContext, CUIRect View, bool Active); @@ -837,7 +844,7 @@ public: void DoQuadEnvelopes(const std::vector &vQuads, IGraphics::CTextureHandle Texture = IGraphics::CTextureHandle()); void DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int pIndex); - void DoQuadPoint(CQuad *pQuad, int QuadIndex, int v); + void DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int v); void SetHotQuadPoint(const std::shared_ptr &pLayer); float TriangleArea(vec2 A, vec2 B, vec2 C); @@ -849,7 +856,7 @@ public: void DoMapEditor(CUIRect View); void DoToolbarLayers(CUIRect Toolbar); void DoToolbarSounds(CUIRect Toolbar); - void DoQuad(CQuad *pQuad, int Index); + void DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index); ColorRGBA GetButtonColor(const void *pID, int Checked); bool ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate); @@ -874,6 +881,7 @@ public: void RenderEnvelopeEditor(CUIRect View); void RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast); + void RenderEditorHistory(CUIRect View); void RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar); @@ -883,7 +891,7 @@ public: void RenderFileDialog(); void SelectGameLayer(); - void SortImages(); + std::vector SortImages(); bool SelectLayerByTile(); void DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const void *pStopButtonID, const void *pSeekBarID, const int SampleID); @@ -1002,6 +1010,17 @@ public: unsigned char m_SwitchNum; unsigned char m_SwitchDelay; + + // Undo/Redo + CEditorHistory m_EditorHistory; + CEditorHistory m_ServerSettingsHistory; + CEditorHistory m_EnvelopeEditorHistory; + CQuadEditTracker m_QuadTracker; + CEnvelopeEditorOperationTracker m_EnvOpTracker; + +private: + void UndoLastAction(); + void RedoLastAction(); }; // make sure to inline this function diff --git a/src/game/editor/editor_action.h b/src/game/editor/editor_action.h new file mode 100644 index 000000000..d4fb124df --- /dev/null +++ b/src/game/editor/editor_action.h @@ -0,0 +1,30 @@ +#ifndef GAME_EDITOR_EDITOR_ACTION_H +#define GAME_EDITOR_EDITOR_ACTION_H + +#include + +class CEditor; + +class IEditorAction +{ +public: + IEditorAction(CEditor *pEditor) : + m_pEditor(pEditor) {} + + IEditorAction() = default; + + virtual ~IEditorAction() = default; + + virtual void Undo() = 0; + virtual void Redo() = 0; + + virtual bool IsEmpty() { return false; } + + const char *DisplayText() const { return m_aDisplayText; } + +protected: + CEditor *m_pEditor; + char m_aDisplayText[256]; +}; + +#endif diff --git a/src/game/editor/editor_actions.cpp b/src/game/editor/editor_actions.cpp new file mode 100644 index 000000000..07da00366 --- /dev/null +++ b/src/game/editor/editor_actions.cpp @@ -0,0 +1,2065 @@ +#include "editor_actions.h" +#include + +CEditorBrushDrawAction::CEditorBrushDrawAction(CEditor *pEditor, int Group) : + IEditorAction(pEditor), m_Group(Group) +{ + auto &Map = pEditor->m_Map; + for(size_t k = 0; k < Map.m_vpGroups[Group]->m_vpLayers.size(); k++) + { + auto pLayer = Map.m_vpGroups[Group]->m_vpLayers[k]; + + if(pLayer->m_Type == LAYERTYPE_TILES) + { + auto pLayerTiles = std::static_pointer_cast(pLayer); + + if(pLayer == Map.m_pTeleLayer) + { + if(!Map.m_pTeleLayer->m_History.empty()) + { + m_TeleTileChanges = std::map(Map.m_pTeleLayer->m_History); + Map.m_pTeleLayer->ClearHistory(); + } + } + else if(pLayer == Map.m_pTuneLayer) + { + if(!Map.m_pTuneLayer->m_History.empty()) + { + m_TuneTileChanges = std::map(Map.m_pTuneLayer->m_History); + Map.m_pTuneLayer->ClearHistory(); + } + } + else if(pLayer == Map.m_pSwitchLayer) + { + if(!Map.m_pSwitchLayer->m_History.empty()) + { + m_SwitchTileChanges = std::map(Map.m_pSwitchLayer->m_History); + Map.m_pSwitchLayer->ClearHistory(); + } + } + else if(pLayer == Map.m_pSpeedupLayer) + { + if(!Map.m_pSpeedupLayer->m_History.empty()) + { + m_SpeedupTileChanges = std::map(Map.m_pSpeedupLayer->m_History); + Map.m_pSpeedupLayer->ClearHistory(); + } + } + + if(!pLayerTiles->m_TilesHistory.empty()) + { + m_vTileChanges.emplace_back(std::make_pair(k, std::map(pLayerTiles->m_TilesHistory))); + pLayerTiles->ClearHistory(); + } + } + } + + SetInfos(); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Brush draw (x%d) on %d layers", m_TotalTilesDrawn, m_TotalLayers); +} + +void CEditorBrushDrawAction::SetInfos() +{ + m_TotalTilesDrawn = 0; + m_TotalLayers = 0; + + // Process normal tiles + for(auto const &Pair : m_vTileChanges) + { + int Layer = Pair.first; + std::shared_ptr pLayer = m_pEditor->m_Map.m_vpGroups[m_Group]->m_vpLayers[Layer]; + m_TotalLayers++; + + if(pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(pLayer); + auto Changes = Pair.second; + for(auto &Change : Changes) + { + m_TotalTilesDrawn += Change.second.size(); + } + } + } + + // Process speedup tiles + for(auto const &SpeedupChange : m_SpeedupTileChanges) + { + m_TotalTilesDrawn += SpeedupChange.second.size(); + } + + // Process tele tiles + for(auto const &TeleChange : m_TeleTileChanges) + { + m_TotalTilesDrawn += TeleChange.second.size(); + } + + // Process switch tiles + for(auto const &SwitchChange : m_SwitchTileChanges) + { + m_TotalTilesDrawn += SwitchChange.second.size(); + } + + // Process tune tiles + for(auto const &TuneChange : m_TuneTileChanges) + { + m_TotalTilesDrawn += TuneChange.second.size(); + } + + m_TotalLayers += !m_SpeedupTileChanges.empty(); + m_TotalLayers += !m_SwitchTileChanges.empty(); + m_TotalLayers += !m_TeleTileChanges.empty(); + m_TotalLayers += !m_TuneTileChanges.empty(); +} + +bool CEditorBrushDrawAction::IsEmpty() +{ + return m_vTileChanges.empty() && m_SpeedupTileChanges.empty() && m_SwitchTileChanges.empty() && m_TeleTileChanges.empty() && m_TuneTileChanges.empty(); +} + +void CEditorBrushDrawAction::Undo() +{ + Apply(true); +} + +void CEditorBrushDrawAction::Redo() +{ + Apply(false); +} + +void CEditorBrushDrawAction::Apply(bool Undo) +{ + auto &Map = m_pEditor->m_Map; + + // Process normal tiles + for(auto const &Pair : m_vTileChanges) + { + int Layer = Pair.first; + std::shared_ptr pLayer = Map.m_vpGroups[m_Group]->m_vpLayers[Layer]; + + if(pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(pLayer); + auto Changes = Pair.second; + for(auto &Change : Changes) + { + int y = Change.first; + auto Line = Change.second; + for(auto &Tile : Line) + { + int x = Tile.first; + STileStateChange State = Tile.second; + pLayerTiles->SetTileIgnoreHistory(x, y, Undo ? State.m_Previous : State.m_Current); + } + } + } + } + + // Process speedup tiles + for(auto const &SpeedupChange : m_SpeedupTileChanges) + { + int y = SpeedupChange.first; + auto Line = SpeedupChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pSpeedupLayer->m_Width + x; + SSpeedupTileStateChange State = Tile.second; + SSpeedupTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_Force = Data.m_Force; + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_MaxSpeed = Data.m_MaxSpeed; + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_Angle = Data.m_Angle; + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_Type = Data.m_Type; + Map.m_pSpeedupLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } + + // Process tele tiles + for(auto const &TeleChange : m_TeleTileChanges) + { + int y = TeleChange.first; + auto Line = TeleChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pTeleLayer->m_Width + x; + STeleTileStateChange State = Tile.second; + STeleTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pTeleLayer->m_pTeleTile[Index].m_Number = Data.m_Number; + Map.m_pTeleLayer->m_pTeleTile[Index].m_Type = Data.m_Type; + Map.m_pTeleLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } + + // Process switch tiles + for(auto const &SwitchChange : m_SwitchTileChanges) + { + int y = SwitchChange.first; + auto Line = SwitchChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pSwitchLayer->m_Width + x; + SSwitchTileStateChange State = Tile.second; + SSwitchTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Number = Data.m_Number; + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Type = Data.m_Type; + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Flags = Data.m_Flags; + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Delay = Data.m_Delay; + Map.m_pSwitchLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } + + // Process tune tiles + for(auto const &TuneChange : m_TuneTileChanges) + { + int y = TuneChange.first; + auto Line = TuneChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pTuneLayer->m_Width + x; + STuneTileStateChange State = Tile.second; + STuneTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pTuneLayer->m_pTuneTile[Index].m_Number = Data.m_Number; + Map.m_pTuneLayer->m_pTuneTile[Index].m_Type = Data.m_Type; + Map.m_pTuneLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } +} + +// ------------------------------------------- + +CEditorActionQuadPlace::CEditorActionQuadPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_vBrush(vBrush) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Quad place (x%d)", (int)m_vBrush.size()); +} + +void CEditorActionQuadPlace::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + for(size_t k = 0; k < m_vBrush.size(); k++) + pLayerQuads->m_vQuads.pop_back(); + + m_pEditor->m_Map.OnModify(); +} +void CEditorActionQuadPlace::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + for(auto &Brush : m_vBrush) + pLayerQuads->m_vQuads.push_back(Brush); + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionSoundPlace::CEditorActionSoundPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_vBrush(vBrush) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Sound place (x%d)", (int)m_vBrush.size()); +} + +void CEditorActionSoundPlace::Undo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + for(size_t k = 0; k < m_vBrush.size(); k++) + pLayerSounds->m_vSources.pop_back(); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionSoundPlace::Redo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + for(auto &Brush : m_vBrush) + pLayerSounds->m_vSources.push_back(Brush); + + m_pEditor->m_Map.OnModify(); +} + +// --------------------------------------------------------------------------------------- + +CEditorActionDeleteQuad::CEditorActionDeleteQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector const &vQuadsIndices, std::vector const &vDeletedQuads) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_vQuadsIndices(vQuadsIndices), m_vDeletedQuads(vDeletedQuads) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete quad (x%d)", (int)m_vDeletedQuads.size()); +} + +void CEditorActionDeleteQuad::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + for(size_t k = 0; k < m_vQuadsIndices.size(); k++) + { + pLayerQuads->m_vQuads.insert(pLayerQuads->m_vQuads.begin() + m_vQuadsIndices[k], m_vDeletedQuads[k]); + } +} + +void CEditorActionDeleteQuad::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + std::vector vQuads(m_vQuadsIndices); + + for(int i = 0; i < (int)vQuads.size(); ++i) + { + pLayerQuads->m_vQuads.erase(pLayerQuads->m_vQuads.begin() + vQuads[i]); + for(int j = i + 1; j < (int)vQuads.size(); ++j) + if(vQuads[j] > vQuads[i]) + vQuads[j]--; + + vQuads.erase(vQuads.begin() + i); + + i--; + } +} + +// --------------------------------------------------------------------------------------- + +CEditorActionEditQuadPoint::CEditorActionEditQuadPoint(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, std::vector const &vPreviousPoints, std::vector const &vCurrentPoints) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_vPreviousPoints(vPreviousPoints), m_vCurrentPoints(vCurrentPoints) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad points"); +} + +void CEditorActionEditQuadPoint::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + for(int k = 0; k < 5; k++) + Quad.m_aPoints[k] = m_vPreviousPoints[k]; +} + +void CEditorActionEditQuadPoint::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + for(int k = 0; k < 5; k++) + Quad.m_aPoints[k] = m_vCurrentPoints[k]; +} + +CEditorActionEditQuadProp::CEditorActionEditQuadProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, EQuadProp Prop, int Previous, int Current) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ + static const char *s_apNames[] = { + "order", + "pos X", + "pos Y", + "pos env", + "pos env offset", + "color env", + "color env offset"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad %s property in layer %d of group %d", s_apNames[(int)m_Prop], m_LayerIndex, m_GroupIndex); +} + +void CEditorActionEditQuadProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditQuadProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditQuadProp::Apply(int Value) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + if(m_Prop == EQuadProp::PROP_POS_ENV) + Quad.m_PosEnv = Value; + else if(m_Prop == EQuadProp::PROP_POS_ENV_OFFSET) + Quad.m_PosEnvOffset = Value; + else if(m_Prop == EQuadProp::PROP_COLOR_ENV) + Quad.m_ColorEnv = Value; + else if(m_Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) + Quad.m_ColorEnvOffset = Value; +} + +CEditorActionEditQuadPointProp::CEditorActionEditQuadPointProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, int PointIndex, EQuadPointProp Prop, int Previous, int Current) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_PointIndex(PointIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ + static const char *s_apNames[] = { + "pos X", + "pos Y", + "color", + "tex U", + "tex V"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad point %s property in layer %d of group %d", s_apNames[(int)m_Prop], m_LayerIndex, m_GroupIndex); +} + +void CEditorActionEditQuadPointProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditQuadPointProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditQuadPointProp::Apply(int Value) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + + if(m_Prop == EQuadPointProp::PROP_COLOR) + { + const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast(Value); + + Quad.m_aColors[m_PointIndex].r = ColorPick.r * 255.0f; + Quad.m_aColors[m_PointIndex].g = ColorPick.g * 255.0f; + Quad.m_aColors[m_PointIndex].b = ColorPick.b * 255.0f; + Quad.m_aColors[m_PointIndex].a = ColorPick.a * 255.0f; + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = ColorPick; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(ColorPick); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + else if(m_Prop == EQuadPointProp::PROP_TEX_U) + { + Quad.m_aTexcoords[m_PointIndex].x = Value; + } + else if(m_Prop == EQuadPointProp::PROP_TEX_V) + { + Quad.m_aTexcoords[m_PointIndex].y = Value; + } +} + +// --------------------------------------------------------------------------------------- + +CEditorActionBulk::CEditorActionBulk(CEditor *pEditor, const std::vector> &vpActions, const char *pDisplay, bool Reverse) : + IEditorAction(pEditor), m_vpActions(vpActions), m_Reverse(Reverse) +{ + // Assuming we only use bulk for actions of same type, if no display was provided + if(!pDisplay) + { + const char *pBaseDisplay = m_vpActions[0]->DisplayText(); + if(m_vpActions.size() == 1) + str_copy(m_aDisplayText, pBaseDisplay); + else + str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s (x%d)", pBaseDisplay, (int)m_vpActions.size()); + } + else + { + str_copy(m_aDisplayText, pDisplay); + } +} + +void CEditorActionBulk::Undo() +{ + if(m_Reverse) + { + for(auto pIt = m_vpActions.rbegin(); pIt != m_vpActions.rend(); pIt++) + { + auto &pAction = *pIt; + pAction->Undo(); + } + } + else + { + for(auto &pAction : m_vpActions) + { + pAction->Undo(); + } + } +} + +void CEditorActionBulk::Redo() +{ + for(auto &pAction : m_vpActions) + { + pAction->Redo(); + } +} + +// --------- + +CEditorActionAutoMap::CEditorActionAutoMap(CEditor *pEditor, int GroupIndex, int LayerIndex, const EditorTileStateChangeHistory &Changes) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Changes(Changes) +{ + ComputeInfos(); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Auto map (x%d)", m_TotalChanges); +} + +void CEditorActionAutoMap::Undo() +{ + Apply(true); +} + +void CEditorActionAutoMap::Redo() +{ + Apply(false); +} + +void CEditorActionAutoMap::Apply(bool Undo) +{ + auto &Map = m_pEditor->m_Map; + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + for(auto &Change : m_Changes) + { + int y = Change.first; + auto Line = Change.second; + for(auto &Tile : Line) + { + int x = Tile.first; + STileStateChange State = Tile.second; + pLayerTiles->SetTileIgnoreHistory(x, y, Undo ? State.m_Previous : State.m_Current); + } + } + + Map.OnModify(); +} + +void CEditorActionAutoMap::ComputeInfos() +{ + m_TotalChanges = 0; + for(auto &Line : m_Changes) + m_TotalChanges += Line.second.size(); +} + +// --------- + +CEditorActionLayerBase::CEditorActionLayerBase(CEditor *pEditor, int GroupIndex, int LayerIndex) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_LayerIndex(LayerIndex) +{ + m_pLayer = pEditor->m_Map.m_vpGroups[GroupIndex]->m_vpLayers[LayerIndex]; +} + +// ---------- + +CEditorActionAddLayer::CEditorActionAddLayer(CEditor *pEditor, int GroupIndex, int LayerIndex, bool Duplicate) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Duplicate(Duplicate) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s %s layer in group %d", m_Duplicate ? "Duplicate" : "New", m_pLayer->TypeName(), m_GroupIndex); +} + +void CEditorActionAddLayer::Undo() +{ + // Undo: remove layer from vector but keep it in case we want to add it back + auto &vLayers = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_vpLayers; + vLayers.erase(vLayers.begin() + m_LayerIndex); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + if(m_LayerIndex >= (int)vLayers.size()) + m_pEditor->SelectLayer(vLayers.size() - 1, m_GroupIndex); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionAddLayer::Redo() +{ + // Redo: add back the removed layer contained in this class + auto &vLayers = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_vpLayers; + vLayers.insert(vLayers.begin() + m_LayerIndex, m_pLayer); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + m_pEditor->SelectLayer(m_LayerIndex, m_GroupIndex); + m_pEditor->m_Map.OnModify(); +} + +CEditorActionDeleteLayer::CEditorActionDeleteLayer(CEditor *pEditor, int GroupIndex, int LayerIndex) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete %s layer of group %d", m_pLayer->TypeName(), m_GroupIndex); +} + +void CEditorActionDeleteLayer::Redo() +{ + // Redo: remove layer from vector but keep it in case we want to add it back + auto &vLayers = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_vpLayers; + + if(m_pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + if(pLayerTiles->m_Front) + m_pEditor->m_Map.m_pFrontLayer = nullptr; + else if(pLayerTiles->m_Tele) + m_pEditor->m_Map.m_pTeleLayer = nullptr; + else if(pLayerTiles->m_Speedup) + m_pEditor->m_Map.m_pSpeedupLayer = nullptr; + else if(pLayerTiles->m_Switch) + m_pEditor->m_Map.m_pSwitchLayer = nullptr; + else if(pLayerTiles->m_Tune) + m_pEditor->m_Map.m_pTuneLayer = nullptr; + } + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->DeleteLayer(m_LayerIndex); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + if(m_LayerIndex >= (int)vLayers.size()) + m_pEditor->SelectLayer(vLayers.size() - 1, m_GroupIndex); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionDeleteLayer::Undo() +{ + // Undo: add back the removed layer contained in this class + auto &vLayers = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_vpLayers; + + if(m_pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + if(pLayerTiles->m_Front) + m_pEditor->m_Map.m_pFrontLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Tele) + m_pEditor->m_Map.m_pTeleLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Speedup) + m_pEditor->m_Map.m_pSpeedupLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Switch) + m_pEditor->m_Map.m_pSwitchLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Tune) + m_pEditor->m_Map.m_pTuneLayer = std::static_pointer_cast(m_pLayer); + } + + vLayers.insert(vLayers.begin() + m_LayerIndex, m_pLayer); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + m_pEditor->SelectLayer(m_LayerIndex, m_GroupIndex); + m_pEditor->m_Map.OnModify(); +} + +CEditorActionGroup::CEditorActionGroup(CEditor *pEditor, int GroupIndex, bool Delete) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_Delete(Delete) +{ + m_pGroup = pEditor->m_Map.m_vpGroups[GroupIndex]; + if(m_Delete) + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete group %d", m_GroupIndex); + else + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New group"); +} + +void CEditorActionGroup::Undo() +{ + if(m_Delete) + { + // Undo: add back the group + m_pEditor->m_Map.m_vpGroups.insert(m_pEditor->m_Map.m_vpGroups.begin() + m_GroupIndex, m_pGroup); + m_pEditor->m_SelectedGroup = m_GroupIndex; + m_pEditor->m_Map.OnModify(); + } + else + { + // Undo: delete the group + m_pEditor->m_Map.DeleteGroup(m_GroupIndex); + m_pEditor->m_SelectedGroup = maximum(0, m_GroupIndex - 1); + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionGroup::Redo() +{ + if(!m_Delete) + { + // Redo: add back the group + m_pEditor->m_Map.m_vpGroups.insert(m_pEditor->m_Map.m_vpGroups.begin() + m_GroupIndex, m_pGroup); + m_pEditor->m_SelectedGroup = m_GroupIndex; + } + else + { + // Redo: delete the group + m_pEditor->m_Map.DeleteGroup(m_GroupIndex); + m_pEditor->m_SelectedGroup = maximum(0, m_GroupIndex - 1); + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditGroupProp::CEditorActionEditGroupProp(CEditor *pEditor, int GroupIndex, EGroupProp Prop, int Previous, int Current) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ + static const char *s_apNames[] = { + "order", + "pos X", + "pos Y", + "para X", + "para Y", + "use clipping", + "clip X", + "clip Y", + "clip W", + "clip H"}; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit group %d %s property", m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditGroupProp::Undo() +{ + auto pGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == EGroupProp::PROP_ORDER) + { + int CurrentOrder = m_Current; + bool Dir = m_Current > m_Previous; + while(CurrentOrder != m_Previous) + { + CurrentOrder = m_pEditor->m_Map.SwapGroups(CurrentOrder, Dir ? CurrentOrder - 1 : CurrentOrder + 1); + } + m_pEditor->m_SelectedGroup = m_Previous; + } + else + Apply(m_Previous); +} + +void CEditorActionEditGroupProp::Redo() +{ + auto pGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == EGroupProp::PROP_ORDER) + { + int CurrentOrder = m_Previous; + bool Dir = m_Previous > m_Current; + while(CurrentOrder != m_Current) + { + CurrentOrder = m_pEditor->m_Map.SwapGroups(CurrentOrder, Dir ? CurrentOrder - 1 : CurrentOrder + 1); + } + m_pEditor->m_SelectedGroup = m_Current; + } + else + Apply(m_Current); +} + +void CEditorActionEditGroupProp::Apply(int Value) +{ + auto pGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == EGroupProp::PROP_POS_X) + pGroup->m_OffsetX = Value; + if(m_Prop == EGroupProp::PROP_POS_Y) + pGroup->m_OffsetY = Value; + if(m_Prop == EGroupProp::PROP_PARA_X) + pGroup->m_ParallaxX = Value; + if(m_Prop == EGroupProp::PROP_PARA_Y) + pGroup->m_ParallaxY = Value; + if(m_Prop == EGroupProp::PROP_USE_CLIPPING) + pGroup->m_UseClipping = Value; + if(m_Prop == EGroupProp::PROP_CLIP_X) + pGroup->m_ClipX = Value; + if(m_Prop == EGroupProp::PROP_CLIP_Y) + pGroup->m_ClipY = Value; + if(m_Prop == EGroupProp::PROP_CLIP_W) + pGroup->m_ClipW = Value; + if(m_Prop == EGroupProp::PROP_CLIP_H) + pGroup->m_ClipH = Value; + + m_pEditor->m_Map.OnModify(); +} + +template +CEditorActionEditLayerPropBase::CEditorActionEditLayerPropBase(CEditor *pEditor, int GroupIndex, int LayerIndex, E Prop, int Previous, int Current) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ +} + +CEditorActionEditLayerProp::CEditorActionEditLayerProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "group", + "order", + "HQ"}; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]); +} + +void CEditorActionEditLayerProp::Undo() +{ + auto pCurrentGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == ELayerProp::PROP_ORDER) + { + m_pEditor->SelectLayer(pCurrentGroup->SwapLayers(m_Current, m_Previous)); + } + else + Apply(m_Previous); +} + +void CEditorActionEditLayerProp::Redo() +{ + auto pCurrentGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == ELayerProp::PROP_ORDER) + { + m_pEditor->SelectLayer(pCurrentGroup->SwapLayers(m_Previous, m_Current)); + } + else + Apply(m_Current); +} + +void CEditorActionEditLayerProp::Apply(int Value) +{ + if(m_Prop == ELayerProp::PROP_GROUP) + { + auto pCurrentGroup = m_pEditor->m_Map.m_vpGroups[Value == m_Previous ? m_Current : m_Previous]; + auto Position = std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), m_pLayer); + if(Position != pCurrentGroup->m_vpLayers.end()) + pCurrentGroup->m_vpLayers.erase(Position); + m_pEditor->m_Map.m_vpGroups[Value]->m_vpLayers.push_back(m_pLayer); + m_pEditor->m_SelectedGroup = Value; + m_pEditor->SelectLayer(m_pEditor->m_Map.m_vpGroups[Value]->m_vpLayers.size() - 1); + } + else if(m_Prop == ELayerProp::PROP_HQ) + { + m_pLayer->m_Flags = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditLayerTilesProp::CEditorActionEditLayerTilesProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ETilesProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "width", + "height", + "shift", + "shift by", + "image", + "color", + "color env", + "color env offset", + "automapper", + "seed"}; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit tiles layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditLayerTilesProp::SetSavedLayers(const std::map> &SavedLayers) +{ + m_SavedLayers = std::map(SavedLayers); +} + +void CEditorActionEditLayerTilesProp::Undo() +{ + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + std::shared_ptr pSavedLayerTiles = nullptr; + + if(m_Prop == ETilesProp::PROP_WIDTH || m_Prop == ETilesProp::PROP_HEIGHT) + { + if(m_Prop == ETilesProp::PROP_HEIGHT) + pLayerTiles->Resize(pLayerTiles->m_Width, m_Previous); + else if(m_Prop == ETilesProp::PROP_WIDTH) + pLayerTiles->Resize(m_Previous, pLayerTiles->m_Height); + + RestoreLayer(LAYERTYPE_TILES, pLayerTiles); + if(pLayerTiles->m_Game || pLayerTiles->m_Front || pLayerTiles->m_Switch || pLayerTiles->m_Speedup || pLayerTiles->m_Tune) + { + if(m_pEditor->m_Map.m_pFrontLayer && !pLayerTiles->m_Front) + RestoreLayer(LAYERTYPE_FRONT, m_pEditor->m_Map.m_pFrontLayer); + if(m_pEditor->m_Map.m_pTeleLayer && !pLayerTiles->m_Tele) + RestoreLayer(LAYERTYPE_TELE, m_pEditor->m_Map.m_pTeleLayer); + if(m_pEditor->m_Map.m_pSwitchLayer && !pLayerTiles->m_Switch) + RestoreLayer(LAYERTYPE_SWITCH, m_pEditor->m_Map.m_pSwitchLayer); + if(m_pEditor->m_Map.m_pSpeedupLayer && !pLayerTiles->m_Speedup) + RestoreLayer(LAYERTYPE_SPEEDUP, m_pEditor->m_Map.m_pSpeedupLayer); + if(m_pEditor->m_Map.m_pTuneLayer && !pLayerTiles->m_Tune) + RestoreLayer(LAYERTYPE_TUNE, m_pEditor->m_Map.m_pTuneLayer); + if(!pLayerTiles->m_Game) + RestoreLayer(LAYERTYPE_GAME, m_pEditor->m_Map.m_pGameLayer); + } + } + else if(m_Prop == ETilesProp::PROP_SHIFT) + { + RestoreLayer(LAYERTYPE_TILES, pLayerTiles); + } + else if(m_Prop == ETilesProp::PROP_SHIFT_BY) + { + m_pEditor->m_ShiftBy = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_IMAGE) + { + if(m_Previous == -1) + { + pLayerTiles->m_Image = -1; + } + else + { + pLayerTiles->m_Image = m_Previous % m_pEditor->m_Map.m_vpImages.size(); + pLayerTiles->m_AutoMapperConfig = -1; + } + } + else if(m_Prop == ETilesProp::PROP_COLOR) + { + const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast(m_Previous); + + pLayerTiles->m_Color.r = ColorPick.r * 255.0f; + pLayerTiles->m_Color.g = ColorPick.g * 255.0f; + pLayerTiles->m_Color.b = ColorPick.b * 255.0f; + pLayerTiles->m_Color.a = ColorPick.a * 255.0f; + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = ColorPick; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(ColorPick); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV) + { + pLayerTiles->m_ColorEnv = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV_OFFSET) + { + pLayerTiles->m_ColorEnvOffset = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_AUTOMAPPER) + { + pLayerTiles->m_AutoMapperConfig = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_SEED) + { + pLayerTiles->m_Seed = m_Previous; + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditLayerTilesProp::Redo() +{ + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + + if(m_Prop == ETilesProp::PROP_WIDTH || m_Prop == ETilesProp::PROP_HEIGHT) + { + if(m_Prop == ETilesProp::PROP_HEIGHT) + pLayerTiles->Resize(pLayerTiles->m_Width, m_Current); + else if(m_Prop == ETilesProp::PROP_WIDTH) + pLayerTiles->Resize(m_Current, pLayerTiles->m_Height); + + if(pLayerTiles->m_Game || pLayerTiles->m_Front || pLayerTiles->m_Switch || pLayerTiles->m_Speedup || pLayerTiles->m_Tune) + { + if(m_pEditor->m_Map.m_pFrontLayer && !pLayerTiles->m_Front) + m_pEditor->m_Map.m_pFrontLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pTeleLayer && !pLayerTiles->m_Tele) + m_pEditor->m_Map.m_pTeleLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pSwitchLayer && !pLayerTiles->m_Switch) + m_pEditor->m_Map.m_pSwitchLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pSpeedupLayer && !pLayerTiles->m_Speedup) + m_pEditor->m_Map.m_pSpeedupLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pTuneLayer && !pLayerTiles->m_Tune) + m_pEditor->m_Map.m_pTuneLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(!pLayerTiles->m_Game) + m_pEditor->m_Map.m_pGameLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + } + } + else if(m_Prop == ETilesProp::PROP_SHIFT) + { + pLayerTiles->Shift(m_Current); + } + else if(m_Prop == ETilesProp::PROP_SHIFT_BY) + { + m_pEditor->m_ShiftBy = m_Current; + } + else if(m_Prop == ETilesProp::PROP_IMAGE) + { + if(m_Current == -1) + { + pLayerTiles->m_Image = -1; + } + else + { + pLayerTiles->m_Image = m_Current % m_pEditor->m_Map.m_vpImages.size(); + pLayerTiles->m_AutoMapperConfig = -1; + } + } + else if(m_Prop == ETilesProp::PROP_COLOR) + { + const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast(m_Current); + + pLayerTiles->m_Color.r = ColorPick.r * 255.0f; + pLayerTiles->m_Color.g = ColorPick.g * 255.0f; + pLayerTiles->m_Color.b = ColorPick.b * 255.0f; + pLayerTiles->m_Color.a = ColorPick.a * 255.0f; + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = ColorPick; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(ColorPick); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV) + { + pLayerTiles->m_ColorEnv = m_Current; + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV_OFFSET) + { + pLayerTiles->m_ColorEnvOffset = m_Current; + } + else if(m_Prop == ETilesProp::PROP_AUTOMAPPER) + { + pLayerTiles->m_AutoMapperConfig = m_Current; + } + else if(m_Prop == ETilesProp::PROP_SEED) + { + pLayerTiles->m_Seed = m_Current; + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditLayerTilesProp::RestoreLayer(int Layer, const std::shared_ptr &pLayerTiles) +{ + if(m_SavedLayers[Layer] != nullptr) + { + std::shared_ptr pSavedLayerTiles = std::static_pointer_cast(m_SavedLayers[Layer]); + mem_copy(pLayerTiles->m_pTiles, pSavedLayerTiles->m_pTiles, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile)); + + if(pLayerTiles->m_Tele) + { + std::shared_ptr pLayerTele = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerTele = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerTele->m_pTeleTile, pSavedLayerTele->m_pTeleTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTeleTile)); + } + else if(pLayerTiles->m_Speedup) + { + std::shared_ptr pLayerSpeedup = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerSpeedup = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerSpeedup->m_pSpeedupTile, pSavedLayerSpeedup->m_pSpeedupTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSpeedupTile)); + } + else if(pLayerTiles->m_Switch) + { + std::shared_ptr pLayerSwitch = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerSwitch = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerSwitch->m_pSwitchTile, pSavedLayerSwitch->m_pSwitchTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSwitchTile)); + } + else if(pLayerTiles->m_Tune) + { + std::shared_ptr pLayerTune = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerTune = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerTune->m_pTuneTile, pSavedLayerTune->m_pTuneTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTuneTile)); + } + } +} + +CEditorActionEditLayerQuadsProp::CEditorActionEditLayerQuadsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerQuadsProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "image"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quads layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]); +} + +void CEditorActionEditLayerQuadsProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditLayerQuadsProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditLayerQuadsProp::Apply(int Value) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + if(m_Prop == ELayerQuadsProp::PROP_IMAGE) + { + if(Value >= 0) + pLayerQuads->m_Image = Value % m_pEditor->m_Map.m_vpImages.size(); + else + pLayerQuads->m_Image = -1; + } + + m_pEditor->m_Map.OnModify(); +} + +// -------------------------------------------------------------- + +CEditorActionEditLayersGroupAndOrder::CEditorActionEditLayersGroupAndOrder(CEditor *pEditor, int GroupIndex, const std::vector &LayerIndices, int NewGroupIndex, const std::vector &NewLayerIndices) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_LayerIndices(LayerIndices), m_NewGroupIndex(NewGroupIndex), m_NewLayerIndices(NewLayerIndices) +{ + std::sort(m_LayerIndices.begin(), m_LayerIndices.end()); + std::sort(m_NewLayerIndices.begin(), m_NewLayerIndices.end()); + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit layers group and order (x%d)", (int)m_LayerIndices.size()); +} + +void CEditorActionEditLayersGroupAndOrder::Undo() +{ + // Undo : restore group and order + auto &Map = m_pEditor->m_Map; + auto &pCurrentGroup = Map.m_vpGroups[m_NewGroupIndex]; + auto &pPreviousGroup = Map.m_vpGroups[m_GroupIndex]; + std::vector> vpLayers; + for(auto &LayerIndex : m_NewLayerIndices) + vpLayers.push_back(pCurrentGroup->m_vpLayers[LayerIndex]); + + int k = 0; + for(auto &pLayer : vpLayers) + { + pCurrentGroup->m_vpLayers.erase(std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pLayer)); + pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_LayerIndices[k++], pLayer); + } + + m_pEditor->m_vSelectedLayers = m_LayerIndices; + m_pEditor->m_SelectedGroup = m_GroupIndex; +} + +void CEditorActionEditLayersGroupAndOrder::Redo() +{ + // Redo : move layers + auto &Map = m_pEditor->m_Map; + auto &pCurrentGroup = Map.m_vpGroups[m_GroupIndex]; + auto &pPreviousGroup = Map.m_vpGroups[m_NewGroupIndex]; + std::vector> vpLayers; + for(auto &LayerIndex : m_LayerIndices) + vpLayers.push_back(pCurrentGroup->m_vpLayers[LayerIndex]); + + int k = 0; + for(auto &pLayer : vpLayers) + { + pCurrentGroup->m_vpLayers.erase(std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pLayer)); + pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_NewLayerIndices[k++], pLayer); + } + + m_pEditor->m_vSelectedLayers = m_NewLayerIndices; + m_pEditor->m_SelectedGroup = m_NewGroupIndex; +} + +// ----------------------------------- + +CEditorActionAppendMap::CEditorActionAppendMap(CEditor *pEditor, const char *pMapName, const SPrevInfo &PrevInfo, std::vector &vImageIndexMap) : + IEditorAction(pEditor), m_PrevInfo(PrevInfo), m_vImageIndexMap(vImageIndexMap) +{ + str_copy(m_aMapName, pMapName); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Append %s", m_aMapName); +} + +void CEditorActionAppendMap::Undo() +{ + auto &Map = m_pEditor->m_Map; + // Undo append: + // - delete added groups + // - delete added envelopes + // - delete added images + // - delete added sounds + + // Delete added groups + while((int)Map.m_vpGroups.size() != m_PrevInfo.m_Groups) + { + Map.m_vpGroups.pop_back(); + } + + // Delete added envelopes + while((int)Map.m_vpEnvelopes.size() != m_PrevInfo.m_Envelopes) + { + Map.m_vpEnvelopes.pop_back(); + } + + // Delete added sounds + while((int)Map.m_vpSounds.size() != m_PrevInfo.m_Sounds) + { + Map.m_vpSounds.pop_back(); + } + + // Delete added images + // Images are sorted when appending, so we need to revert sorting before deleting the images + if(!m_vImageIndexMap.empty()) + { + std::vector vReverseIndexMap; + vReverseIndexMap.resize(m_vImageIndexMap.size()); + + for(int k = 0; k < (int)m_vImageIndexMap.size(); k++) + vReverseIndexMap[m_vImageIndexMap[k]] = k; + + std::vector> vpRevertedImages; + vpRevertedImages.resize(Map.m_vpImages.size()); + + for(int k = 0; k < (int)vReverseIndexMap.size(); k++) + { + vpRevertedImages[vReverseIndexMap[k]] = Map.m_vpImages[k]; + } + Map.m_vpImages = vpRevertedImages; + + Map.ModifyImageIndex([vReverseIndexMap](int *pIndex) { + if(*pIndex >= 0) + { + *pIndex = vReverseIndexMap[*pIndex]; + } + }); + } + + while((int)Map.m_vpImages.size() != m_PrevInfo.m_Images) + { + Map.m_vpImages.pop_back(); + } +} + +void CEditorActionAppendMap::Redo() +{ + // Redo is just re-appending the same map + m_pEditor->Append(m_aMapName, IStorage::TYPE_ALL, true); +} + +// --------------------------- + +CEditorActionTileArt::CEditorActionTileArt(CEditor *pEditor, int PreviousImageCount, const char *pTileArtFile, std::vector &vImageIndexMap) : + IEditorAction(pEditor), m_PreviousImageCount(PreviousImageCount), m_vImageIndexMap(vImageIndexMap) +{ + str_copy(m_aTileArtFile, pTileArtFile); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Tile art"); +} + +void CEditorActionTileArt::Undo() +{ + auto &Map = m_pEditor->m_Map; + + // Delete added group + Map.m_vpGroups.pop_back(); + + // Delete added images + // Images are sorted when appending, so we need to revert sorting before deleting the images + if(!m_vImageIndexMap.empty()) + { + std::vector vReverseIndexMap; + vReverseIndexMap.resize(m_vImageIndexMap.size()); + + for(int k = 0; k < (int)m_vImageIndexMap.size(); k++) + vReverseIndexMap[m_vImageIndexMap[k]] = k; + + std::vector> vpRevertedImages; + vpRevertedImages.resize(Map.m_vpImages.size()); + + for(int k = 0; k < (int)vReverseIndexMap.size(); k++) + { + vpRevertedImages[vReverseIndexMap[k]] = Map.m_vpImages[k]; + } + Map.m_vpImages = vpRevertedImages; + + Map.ModifyImageIndex([vReverseIndexMap](int *pIndex) { + if(*pIndex >= 0) + { + *pIndex = vReverseIndexMap[*pIndex]; + } + }); + } + + while((int)Map.m_vpImages.size() != m_PreviousImageCount) + { + Map.m_vpImages.pop_back(); + } +} + +void CEditorActionTileArt::Redo() +{ + if(!m_pEditor->Graphics()->LoadPNG(&m_pEditor->m_TileartImageInfo, m_aTileArtFile, IStorage::TYPE_ALL)) + { + m_pEditor->ShowFileDialogError("Failed to load image from file '%s'.", m_aTileArtFile); + return; + } + + IStorage::StripPathAndExtension(m_aTileArtFile, m_pEditor->m_aTileartFilename, sizeof(m_pEditor->m_aTileartFilename)); + m_pEditor->AddTileart(true); +} + +// --------------------------------- + +CEditorCommandAction::CEditorCommandAction(CEditor *pEditor, EType Type, int *pSelectedCommandIndex, int CommandIndex, const char *pPreviousCommand, const char *pCurrentCommand) : + IEditorAction(pEditor), m_Type(Type), m_pSelectedCommandIndex(pSelectedCommandIndex), m_CommandIndex(CommandIndex) +{ + if(pPreviousCommand != nullptr) + m_PreviousCommand = std::string(pPreviousCommand); + if(pCurrentCommand != nullptr) + m_CurrentCommand = std::string(pCurrentCommand); + + switch(m_Type) + { + case EType::ADD: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add command"); + break; + case EType::EDIT: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex); + break; + case EType::DELETE: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete command %d", m_CommandIndex); + break; + case EType::MOVE_UP: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move command %d up", m_CommandIndex); + break; + case EType::MOVE_DOWN: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move command %d down", m_CommandIndex); + break; + default: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex); + break; + } +} + +void CEditorCommandAction::Undo() +{ + auto &Map = m_pEditor->m_Map; + switch(m_Type) + { + case EType::DELETE: + { + Map.m_vSettings.insert(Map.m_vSettings.begin() + m_CommandIndex, m_PreviousCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::ADD: + { + Map.m_vSettings.erase(Map.m_vSettings.begin() + m_CommandIndex); + *m_pSelectedCommandIndex = Map.m_vSettings.size() - 1; + break; + } + case EType::EDIT: + { + str_copy(Map.m_vSettings[m_CommandIndex].m_aCommand, m_PreviousCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_DOWN: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex + 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_UP: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex - 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + } +} + +void CEditorCommandAction::Redo() +{ + auto &Map = m_pEditor->m_Map; + switch(m_Type) + { + case EType::DELETE: + { + Map.m_vSettings.erase(Map.m_vSettings.begin() + m_CommandIndex); + *m_pSelectedCommandIndex = Map.m_vSettings.size() - 1; + break; + } + case EType::ADD: + { + Map.m_vSettings.insert(Map.m_vSettings.begin() + m_CommandIndex, m_PreviousCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::EDIT: + { + str_copy(Map.m_vSettings[m_CommandIndex].m_aCommand, m_CurrentCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_DOWN: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex + 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_UP: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex - 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + } +} + +// ------------------------------------------------ + +CEditorActionEnvelopeAdd::CEditorActionEnvelopeAdd(CEditor *pEditor, const std::shared_ptr &pEnv) : + IEditorAction(pEditor), m_pEnv(pEnv) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add new %s envelope", pEnv->Type() == CEnvelope::EType::COLOR ? "color" : (pEnv->Type() == CEnvelope::EType::POSITION ? "position" : "sound")); +} + +void CEditorActionEnvelopeAdd::Undo() +{ + // Undo is removing the envelope, which was added at the back of the list + m_pEditor->m_Map.m_vpEnvelopes.pop_back(); + m_pEditor->m_SelectedEnvelope = m_pEditor->m_Map.m_vpEnvelopes.size() - 1; +} + +void CEditorActionEnvelopeAdd::Redo() +{ + // Redo is adding back at the back the saved envelope + m_pEditor->m_Map.m_vpEnvelopes.push_back(m_pEnv); + m_pEditor->m_SelectedEnvelope = m_pEditor->m_Map.m_vpEnvelopes.size() - 1; +} + +CEditorActionEveloppeDelete::CEditorActionEveloppeDelete(CEditor *pEditor, int EnvelopeIndex) : + IEditorAction(pEditor), m_EnvelopeIndex(EnvelopeIndex), m_pEnv(pEditor->m_Map.m_vpEnvelopes[EnvelopeIndex]) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete envelope %d", m_EnvelopeIndex); +} + +void CEditorActionEveloppeDelete::Undo() +{ + // Undo is adding back the envelope + m_pEditor->m_Map.m_vpEnvelopes.insert(m_pEditor->m_Map.m_vpEnvelopes.begin() + m_EnvelopeIndex, m_pEnv); + m_pEditor->m_SelectedEnvelope = m_EnvelopeIndex; +} + +void CEditorActionEveloppeDelete::Redo() +{ + // Redo is erasing the same envelope index + m_pEditor->m_Map.m_vpEnvelopes.erase(m_pEditor->m_Map.m_vpEnvelopes.begin() + m_EnvelopeIndex); + if(m_pEditor->m_SelectedEnvelope >= (int)m_pEditor->m_Map.m_vpEnvelopes.size()) + m_pEditor->m_SelectedEnvelope = m_pEditor->m_Map.m_vpEnvelopes.size() - 1; +} + +CEditorActionEnvelopeEdit::CEditorActionEnvelopeEdit(CEditor *pEditor, int EnvelopeIndex, EEditType EditType, int Previous, int Current) : + IEditorAction(pEditor), m_EnvelopeIndex(EnvelopeIndex), m_EditType(EditType), m_Previous(Previous), m_Current(Current), m_pEnv(pEditor->m_Map.m_vpEnvelopes[EnvelopeIndex]) +{ + static const char *s_apNames[] = { + "sync", + "order"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit envelope %d %s", m_EnvelopeIndex, s_apNames[(int)m_EditType]); +} + +void CEditorActionEnvelopeEdit::Undo() +{ + switch(m_EditType) + { + case EEditType::ORDER: + { + m_pEditor->m_Map.SwapEnvelopes(m_Current, m_Previous); + break; + } + case EEditType::SYNC: + { + m_pEnv->m_Synchronized = m_Previous; + break; + } + } + m_pEditor->m_Map.OnModify(); + m_pEditor->m_SelectedEnvelope = m_EnvelopeIndex; +} + +void CEditorActionEnvelopeEdit::Redo() +{ + switch(m_EditType) + { + case EEditType::ORDER: + { + m_pEditor->m_Map.SwapEnvelopes(m_Previous, m_Current); + break; + } + case EEditType::SYNC: + { + m_pEnv->m_Synchronized = m_Current; + break; + } + } + m_pEditor->m_Map.OnModify(); + m_pEditor->m_SelectedEnvelope = m_EnvelopeIndex; +} + +CEditorActionEnvelopeEditPoint::CEditorActionEnvelopeEditPoint(CEditor *pEditor, int EnvelopeIndex, int PointIndex, int Channel, EEditType EditType, int Previous, int Current) : + IEditorAction(pEditor), m_EnvelopeIndex(EnvelopeIndex), m_PointIndex(PointIndex), m_Channel(Channel), m_EditType(EditType), m_Previous(Previous), m_Current(Current), m_pEnv(pEditor->m_Map.m_vpEnvelopes[EnvelopeIndex]) +{ + static const char *s_apNames[] = { + "time", + "value", + "curve type"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit %s of point %d (channel %d) of env %d", s_apNames[(int)m_EditType], m_PointIndex, m_Channel, m_EnvelopeIndex); +} + +void CEditorActionEnvelopeEditPoint::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEnvelopeEditPoint::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEnvelopeEditPoint::Apply(int Value) +{ + if(m_EditType == EEditType::TIME) + { + m_pEnv->m_vPoints[m_PointIndex].m_Time = Value; + } + else if(m_EditType == EEditType::VALUE) + { + m_pEnv->m_vPoints[m_PointIndex].m_aValues[m_Channel] = Value; + + if(m_pEnv->GetChannels() == 4) + { + auto *pValues = m_pEnv->m_vPoints[m_PointIndex].m_aValues; + const ColorRGBA Color = ColorRGBA(fx2f(pValues[0]), fx2f(pValues[1]), fx2f(pValues[2]), fx2f(pValues[3])); + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = Color; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(Color); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + } + else if(m_EditType == EEditType::CURVE_TYPE) + { + m_pEnv->m_vPoints[m_PointIndex].m_Curvetype = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +// ---- + +CEditorActionEditEnvelopePointValue::CEditorActionEditEnvelopePointValue(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, EType Type, int OldTime, int OldValue, int NewTime, int NewValue) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_PtIndex(PointIndex), m_Channel(Channel), m_Type(Type), m_OldTime(OldTime), m_OldValue(OldValue), m_NewTime(NewTime), m_NewValue(NewValue) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit point %d%s value (envelope %d, channel %d)", PointIndex, m_Type == EType::TANGENT_IN ? "tangent in" : (m_Type == EType::TANGENT_OUT ? "tangent out" : ""), m_EnvIndex, m_Channel); +} + +void CEditorActionEditEnvelopePointValue::Undo() +{ + Apply(true); +} + +void CEditorActionEditEnvelopePointValue::Redo() +{ + Apply(false); +} + +void CEditorActionEditEnvelopePointValue::Apply(bool Undo) +{ + float CurrentValue = fx2f(Undo ? m_OldValue : m_NewValue); + float CurrentTime = (Undo ? m_OldTime : m_NewTime) / 1000.0f; + + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + if(m_Type == EType::TANGENT_IN) + { + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = minimum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[m_PtIndex].m_Time, 0); + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[m_PtIndex].m_aValues[m_Channel]; + } + else if(m_Type == EType::TANGENT_OUT) + { + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = maximum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[m_PtIndex].m_Time, 0); + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[m_PtIndex].m_aValues[m_Channel]; + } + else + { + if(pEnvelope->GetChannels() == 4) + CurrentValue = clamp(CurrentValue, 0.0f, 1.0f); + pEnvelope->m_vPoints[m_PtIndex].m_aValues[m_Channel] = f2fx(CurrentValue); + + if(m_PtIndex != 0) + { + pEnvelope->m_vPoints[m_PtIndex].m_Time = CurrentTime * 1000.0f; + + if(pEnvelope->m_vPoints[m_PtIndex].m_Time < pEnvelope->m_vPoints[m_PtIndex - 1].m_Time) + pEnvelope->m_vPoints[m_PtIndex].m_Time = pEnvelope->m_vPoints[m_PtIndex - 1].m_Time + 1; + if(static_cast(m_PtIndex) + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[m_PtIndex].m_Time > pEnvelope->m_vPoints[m_PtIndex + 1].m_Time) + pEnvelope->m_vPoints[m_PtIndex].m_Time = pEnvelope->m_vPoints[m_PtIndex + 1].m_Time - 1; + } + else + { + pEnvelope->m_vPoints[m_PtIndex].m_Time = 0.0f; + } + } + + m_pEditor->m_Map.OnModify(); + m_pEditor->m_UpdateEnvPointInfo = true; +} + +// --------------------- + +CEditorActionResetEnvelopePointTangent::CEditorActionResetEnvelopePointTangent(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, bool In) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_PointIndex(PointIndex), m_Channel(Channel), m_In(In) +{ + std::shared_ptr pEnvelope = pEditor->m_Map.m_vpEnvelopes[EnvIndex]; + if(In) + { + m_Previous[0] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aInTangentDeltaX[Channel]; + m_Previous[1] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aInTangentDeltaY[Channel]; + } + else + { + m_Previous[0] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aOutTangentDeltaX[Channel]; + m_Previous[1] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aOutTangentDeltaY[Channel]; + } + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Reset point %d of env %d tangent %s", m_PointIndex, m_EnvIndex, m_In ? "in" : "out"); +} + +void CEditorActionResetEnvelopePointTangent::Undo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + if(m_In) + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = m_Previous[0]; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = m_Previous[1]; + } + else + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = m_Previous[0]; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = m_Previous[1]; + } + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionResetEnvelopePointTangent::Redo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + if(m_In) + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = 0.0f; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = 0.0f; + } + else + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = 0.0f; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = 0.0f; + } + m_pEditor->m_Map.OnModify(); +} + +// ------------------ + +CEditorActionAddEnvelopePoint::CEditorActionAddEnvelopePoint(CEditor *pEditor, int EnvIndex, int Time, ColorRGBA Channels) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_Time(Time), m_Channels(Channels) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add new point in envelope %d at time %f", m_EnvIndex, Time / 1000.0); +} + +void CEditorActionAddEnvelopePoint::Undo() +{ + // Delete added point + auto pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + auto pIt = std::find_if(pEnvelope->m_vPoints.begin(), pEnvelope->m_vPoints.end(), [this](const CEnvPoint_runtime &Point) { + return Point.m_Time == m_Time; + }); + if(pIt != pEnvelope->m_vPoints.end()) + { + pEnvelope->m_vPoints.erase(pIt); + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionAddEnvelopePoint::Redo() +{ + auto pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + pEnvelope->AddPoint(m_Time, + f2fx(m_Channels.r), f2fx(m_Channels.g), + f2fx(m_Channels.b), f2fx(m_Channels.a)); + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionDeleteEnvelopePoint::CEditorActionDeleteEnvelopePoint(CEditor *pEditor, int EnvIndex, int PointIndex) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_PointIndex(PointIndex), m_Point(pEditor->m_Map.m_vpEnvelopes[EnvIndex]->m_vPoints[PointIndex]) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete point %d of envelope %d", m_PointIndex, m_EnvIndex); +} + +void CEditorActionDeleteEnvelopePoint::Undo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + pEnvelope->m_vPoints.insert(pEnvelope->m_vPoints.begin() + m_PointIndex, m_Point); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionDeleteEnvelopePoint::Redo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + m_PointIndex); + + auto pSelectedPointIt = std::find_if(m_pEditor->m_vSelectedEnvelopePoints.begin(), m_pEditor->m_vSelectedEnvelopePoints.end(), [this](const std::pair Pair) { + return Pair.first == m_PointIndex; + }); + + if(pSelectedPointIt != m_pEditor->m_vSelectedEnvelopePoints.end()) + m_pEditor->m_vSelectedEnvelopePoints.erase(pSelectedPointIt); + + m_pEditor->m_Map.OnModify(); +} + +// ------------------------------- + +CEditorActionEditLayerSoundsProp::CEditorActionEditLayerSoundsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerSoundsProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "sound"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sounds layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]); +} + +void CEditorActionEditLayerSoundsProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditLayerSoundsProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditLayerSoundsProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + if(m_Prop == ELayerSoundsProp::PROP_SOUND) + { + if(Value >= 0) + pLayerSounds->m_Sound = Value % m_pEditor->m_Map.m_vpSounds.size(); + else + pLayerSounds->m_Sound = -1; + } + + m_pEditor->m_Map.OnModify(); +} + +// --- + +CEditorActionDeleteSoundSource::CEditorActionDeleteSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + m_Source = pLayerSounds->m_vSources[SourceIndex]; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete sound source %d in layer %d of group %d", SourceIndex, LayerIndex, GroupIndex); +} + +void CEditorActionDeleteSoundSource::Undo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->m_vSources.insert(pLayerSounds->m_vSources.begin() + m_SourceIndex, m_Source); + m_pEditor->m_SelectedSource = m_SourceIndex; + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionDeleteSoundSource::Redo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->m_vSources.erase(pLayerSounds->m_vSources.begin() + m_SourceIndex); + m_pEditor->m_SelectedSource--; + m_pEditor->m_Map.OnModify(); +} + +// --------------- + +CEditorActionEditSoundSource::CEditorActionEditSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, EEditType Type, int Value) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex), m_EditType(Type), m_CurrentValue(Value), m_pSavedObject(nullptr) +{ + Save(); + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d", SourceIndex, LayerIndex, GroupIndex); +} + +void CEditorActionEditSoundSource::Undo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_EditType == EEditType::SHAPE) + { + CSoundShape *pSavedShape = (CSoundShape *)m_pSavedObject; + pSource->m_Shape.m_Type = pSavedShape->m_Type; + + // set default values + switch(pSource->m_Shape.m_Type) + { + case CSoundShape::SHAPE_CIRCLE: + { + pSource->m_Shape.m_Circle.m_Radius = pSavedShape->m_Circle.m_Radius; + break; + } + case CSoundShape::SHAPE_RECTANGLE: + { + pSource->m_Shape.m_Rectangle.m_Width = pSavedShape->m_Rectangle.m_Width; + pSource->m_Shape.m_Rectangle.m_Height = pSavedShape->m_Rectangle.m_Height; + break; + } + } + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditSoundSource::Redo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_EditType == EEditType::SHAPE) + { + pSource->m_Shape.m_Type = m_CurrentValue; + + // set default values + switch(pSource->m_Shape.m_Type) + { + case CSoundShape::SHAPE_CIRCLE: + { + pSource->m_Shape.m_Circle.m_Radius = 1000.0f; + break; + } + case CSoundShape::SHAPE_RECTANGLE: + { + pSource->m_Shape.m_Rectangle.m_Width = f2fx(1000.0f); + pSource->m_Shape.m_Rectangle.m_Height = f2fx(800.0f); + break; + } + } + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditSoundSource::Save() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_EditType == EEditType::SHAPE) + { + CSoundShape *pShapeInfo = new CSoundShape; + pShapeInfo->m_Type = pSource->m_Shape.m_Type; + + switch(pSource->m_Shape.m_Type) + { + case CSoundShape::SHAPE_CIRCLE: + { + pShapeInfo->m_Circle.m_Radius = pSource->m_Shape.m_Circle.m_Radius; + break; + } + case CSoundShape::SHAPE_RECTANGLE: + { + pShapeInfo->m_Rectangle.m_Width = pSource->m_Shape.m_Rectangle.m_Width; + pShapeInfo->m_Rectangle.m_Height = pSource->m_Shape.m_Rectangle.m_Height; + break; + } + } + + m_pSavedObject = pShapeInfo; + } +} + +CEditorActionEditSoundSource::~CEditorActionEditSoundSource() +{ + if(m_EditType == EEditType::SHAPE) + { + CSoundShape *pSavedShape = (CSoundShape *)m_pSavedObject; + delete pSavedShape; + } +} + +// ----- + +CEditorActionEditSoundSourceProp::CEditorActionEditSoundSourceProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ESoundProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex) +{ + static const char *s_apNames[] = { + "pos X", + "pos Y", + "loop", + "pan", + "time delay", + "falloff", + "pos env", + "pos env offset", + "sound env", + "sound env offset"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d %s property", SourceIndex, LayerIndex, GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditSoundSourceProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditSoundSourceProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditSoundSourceProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_Prop == ESoundProp::PROP_POS_X) + { + pSource->m_Position.x = Value; + } + else if(m_Prop == ESoundProp::PROP_POS_Y) + { + pSource->m_Position.y = Value; + } + else if(m_Prop == ESoundProp::PROP_LOOP) + { + pSource->m_Loop = Value; + } + else if(m_Prop == ESoundProp::PROP_PAN) + { + pSource->m_Pan = Value; + } + else if(m_Prop == ESoundProp::PROP_TIME_DELAY) + { + pSource->m_TimeDelay = Value; + } + else if(m_Prop == ESoundProp::PROP_FALLOFF) + { + pSource->m_Falloff = Value; + } + else if(m_Prop == ESoundProp::PROP_POS_ENV) + { + pSource->m_PosEnv = Value; + } + else if(m_Prop == ESoundProp::PROP_POS_ENV_OFFSET) + { + pSource->m_PosEnvOffset = Value; + } + else if(m_Prop == ESoundProp::PROP_SOUND_ENV) + { + pSource->m_SoundEnv = Value; + } + else if(m_Prop == ESoundProp::PROP_SOUND_ENV_OFFSET) + { + pSource->m_SoundEnvOffset = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditRectSoundSourceShapeProp::CEditorActionEditRectSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ERectangleShapeProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex) +{ + static const char *s_apNames[] = { + "width", + "height"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d sound shape %s property", m_SourceIndex, m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditRectSoundSourceShapeProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditRectSoundSourceShapeProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditRectSoundSourceShapeProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_Prop == ERectangleShapeProp::PROP_RECTANGLE_WIDTH) + { + pSource->m_Shape.m_Rectangle.m_Width = Value; + } + else if(m_Prop == ERectangleShapeProp::PROP_RECTANGLE_HEIGHT) + { + pSource->m_Shape.m_Rectangle.m_Height = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditCircleSoundSourceShapeProp::CEditorActionEditCircleSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ECircleShapeProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex) +{ + static const char *s_apNames[] = { + "radius"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d sound shape %s property", m_SourceIndex, m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditCircleSoundSourceShapeProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditCircleSoundSourceShapeProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditCircleSoundSourceShapeProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_Prop == ECircleShapeProp::PROP_CIRCLE_RADIUS) + { + pSource->m_Shape.m_Circle.m_Radius = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +// -------------------------- + +CEditorActionNewEmptySound::CEditorActionNewEmptySound(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_X(x), m_Y(y) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New sound in layer %d of group %d", LayerIndex, GroupIndex); +} + +void CEditorActionNewEmptySound::Undo() +{ + // Undo is simply deleting the added source + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->m_vSources.pop_back(); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionNewEmptySound::Redo() +{ + auto &Map = m_pEditor->m_Map; + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->NewSource(m_X, m_Y); + + Map.OnModify(); +} + +CEditorActionNewEmptyQuad::CEditorActionNewEmptyQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_X(x), m_Y(y) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New quad in layer %d of group %d", LayerIndex, GroupIndex); +} + +void CEditorActionNewEmptyQuad::Undo() +{ + // Undo is simply deleting the added quad + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + pLayerQuads->m_vQuads.pop_back(); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionNewEmptyQuad::Redo() +{ + auto &Map = m_pEditor->m_Map; + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + + int Width = 64; + int Height = 64; + if(pLayerQuads->m_Image >= 0) + { + Width = Map.m_vpImages[pLayerQuads->m_Image]->m_Width; + Height = Map.m_vpImages[pLayerQuads->m_Image]->m_Height; + } + + pLayerQuads->NewQuad(m_X, m_Y, Width, Height); + + Map.OnModify(); +} + +// ------------- + +CEditorActionNewQuad::CEditorActionNewQuad(CEditor *pEditor, int GroupIndex, int LayerIndex) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + m_Quad = pLayerQuads->m_vQuads[pLayerQuads->m_vQuads.size() - 1]; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New quad in layer %d of group %d", LayerIndex, GroupIndex); +} + +void CEditorActionNewQuad::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + pLayerQuads->m_vQuads.pop_back(); +} + +void CEditorActionNewQuad::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + pLayerQuads->m_vQuads.emplace_back(m_Quad); +} diff --git a/src/game/editor/editor_actions.h b/src/game/editor/editor_actions.h new file mode 100644 index 000000000..198e02a6f --- /dev/null +++ b/src/game/editor/editor_actions.h @@ -0,0 +1,648 @@ +#ifndef GAME_EDITOR_EDITOR_ACTIONS_H +#define GAME_EDITOR_EDITOR_ACTIONS_H + +#include "editor.h" +#include "editor_action.h" + +class CEditorActionLayerBase : public IEditorAction +{ +public: + CEditorActionLayerBase(CEditor *pEditor, int GroupIndex, int LayerIndex); + + virtual void Undo() override {} + virtual void Redo() override {} + +protected: + int m_GroupIndex; + int m_LayerIndex; + std::shared_ptr m_pLayer; +}; + +class CEditorBrushDrawAction : public IEditorAction +{ +public: + CEditorBrushDrawAction(CEditor *pEditor, int Group); + + void Undo() override; + void Redo() override; + bool IsEmpty() override; + +private: + int m_Group; + // m_vTileChanges is a list of changes for each layer that was modified. + // The std::pair is used to pair one layer (index) with its history (2D map). + // EditorTileStateChangeHistory is a 2D map, storing a change item at a specific y,x position. + std::vector>> m_vTileChanges; + EditorTileStateChangeHistory m_TeleTileChanges; + EditorTileStateChangeHistory m_SpeedupTileChanges; + EditorTileStateChangeHistory m_SwitchTileChanges; + EditorTileStateChangeHistory m_TuneTileChanges; + + int m_TotalTilesDrawn; + int m_TotalLayers; + + void Apply(bool Undo); + void SetInfos(); +}; + +// --------------------------------------------------------- + +class CEditorActionQuadPlace : public CEditorActionLayerBase +{ +public: + CEditorActionQuadPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush); + + void Undo() override; + void Redo() override; + +private: + std::vector m_vBrush; +}; + +class CEditorActionSoundPlace : public CEditorActionLayerBase +{ +public: + CEditorActionSoundPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush); + + void Undo() override; + void Redo() override; + +private: + std::vector m_vBrush; +}; + +// ------------------------------------------------------------- + +class CEditorActionDeleteQuad : public CEditorActionLayerBase +{ +public: + CEditorActionDeleteQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector const &vQuadsIndices, std::vector const &vDeletedQuads); + + void Undo() override; + void Redo() override; + +private: + std::vector m_vQuadsIndices; + std::vector m_vDeletedQuads; +}; + +// ------------------------------------------------------------- + +class CEditorActionEditQuadPoint : public CEditorActionLayerBase +{ +public: + CEditorActionEditQuadPoint(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, std::vector const &vPreviousPoints, std::vector const &vCurrentPoints); + + void Undo() override; + void Redo() override; + +private: + int m_QuadIndex; + std::vector m_vPreviousPoints; + std::vector m_vCurrentPoints; +}; + +class CEditorActionEditQuadProp : public CEditorActionLayerBase +{ +public: + CEditorActionEditQuadProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, EQuadProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_QuadIndex; + EQuadProp m_Prop; + int m_Previous; + int m_Current; + + void Apply(int Value); +}; + +class CEditorActionEditQuadPointProp : public CEditorActionLayerBase +{ +public: + CEditorActionEditQuadPointProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, int PointIndex, EQuadPointProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_QuadIndex; + int m_PointIndex; + EQuadPointProp m_Prop; + int m_Previous; + int m_Current; + + void Apply(int Value); +}; + +// ------------------------------------------------------------- + +class CEditorActionBulk : public IEditorAction +{ +public: + CEditorActionBulk(CEditor *pEditor, const std::vector> &vpActions, const char *pDisplay = nullptr, bool Reverse = false); + + void Undo() override; + void Redo() override; + +private: + std::vector> m_vpActions; + std::string m_Display; + bool m_Reverse; +}; + +// + +class CEditorActionAutoMap : public CEditorActionLayerBase +{ +public: + CEditorActionAutoMap(CEditor *pEditor, int GroupIndex, int LayerIndex, const EditorTileStateChangeHistory &Changes); + + void Undo() override; + void Redo() override; + +private: + EditorTileStateChangeHistory m_Changes; + int m_TotalChanges; + + void ComputeInfos(); + void Apply(bool Undo); +}; + +// ------------------------------------------------------------- + +class CEditorActionAddLayer : public CEditorActionLayerBase +{ +public: + CEditorActionAddLayer(CEditor *pEditor, int GroupIndex, int LayerIndex, bool Duplicate = false); + + void Undo() override; + void Redo() override; + +private: + bool m_Duplicate; +}; + +class CEditorActionDeleteLayer : public CEditorActionLayerBase +{ +public: + CEditorActionDeleteLayer(CEditor *pEditor, int GroupIndex, int LayerIndex); + + void Undo() override; + void Redo() override; +}; + +class CEditorActionGroup : public IEditorAction +{ +public: + CEditorActionGroup(CEditor *pEditor, int GroupIndex, bool Delete); + + void Undo() override; + void Redo() override; + +private: + int m_GroupIndex; + bool m_Delete; + std::shared_ptr m_pGroup; +}; + +class CEditorActionEditGroupProp : public IEditorAction +{ +public: + CEditorActionEditGroupProp(CEditor *pEditor, int GroupIndex, EGroupProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_GroupIndex; + EGroupProp m_Prop; + int m_Previous; + int m_Current; + + void Apply(int Value); +}; + +template +class CEditorActionEditLayerPropBase : public CEditorActionLayerBase +{ +public: + CEditorActionEditLayerPropBase(CEditor *pEditor, int GroupIndex, int LayerIndex, E Prop, int Previous, int Current); + + virtual void Undo() override {} + virtual void Redo() override {} + +protected: + E m_Prop; + int m_Previous; + int m_Current; +}; + +class CEditorActionEditLayerProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + void Apply(int Value); +}; + +class CEditorActionEditLayerTilesProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerTilesProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ETilesProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + + void SetSavedLayers(const std::map> &SavedLayers); + +private: + std::map> m_SavedLayers; + + void RestoreLayer(int Layer, const std::shared_ptr &pLayerTiles); +}; + +class CEditorActionEditLayerQuadsProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerQuadsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerQuadsProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + void Apply(int Value); +}; + +class CEditorActionEditLayersGroupAndOrder : public IEditorAction +{ +public: + CEditorActionEditLayersGroupAndOrder(CEditor *pEditor, int GroupIndex, const std::vector &LayerIndices, int NewGroupIndex, const std::vector &NewLayerIndices); + + void Undo() override; + void Redo() override; + +private: + int m_GroupIndex; + std::vector m_LayerIndices; + int m_NewGroupIndex; + std::vector m_NewLayerIndices; +}; + +// -------------- + +class CEditorActionAppendMap : public IEditorAction +{ +public: + struct SPrevInfo + { + int m_Groups; + int m_Images; + int m_Sounds; + int m_Envelopes; + }; + +public: + CEditorActionAppendMap(CEditor *pEditor, const char *pMapName, const SPrevInfo &PrevInfo, std::vector &vImageIndexMap); + + void Undo() override; + void Redo() override; + +private: + char m_aMapName[IO_MAX_PATH_LENGTH]; + SPrevInfo m_PrevInfo; + std::vector m_vImageIndexMap; +}; + +// -------------- + +class CEditorActionTileArt : public IEditorAction +{ +public: + CEditorActionTileArt(CEditor *pEditor, int PreviousImageCount, const char *pTileArtFile, std::vector &vImageIndexMap); + + void Undo() override; + void Redo() override; + +private: + int m_PreviousImageCount; + char m_aTileArtFile[IO_MAX_PATH_LENGTH]; + std::vector m_vImageIndexMap; +}; + +// ---------------------- + +class CEditorCommandAction : public IEditorAction +{ +public: + enum class EType + { + DELETE, + ADD, + EDIT, + MOVE_UP, + MOVE_DOWN + }; + + CEditorCommandAction(CEditor *pEditor, EType Type, int *pSelectedCommandIndex, int CommandIndex, const char *pPreviousCommand = nullptr, const char *pCurrentCommand = nullptr); + + void Undo() override; + void Redo() override; + +private: + EType m_Type; + int *m_pSelectedCommandIndex; + int m_CommandIndex; + std::string m_PreviousCommand; + std::string m_CurrentCommand; +}; + +// ------------------------------ + +class CEditorActionEnvelopeAdd : public IEditorAction +{ +public: + CEditorActionEnvelopeAdd(CEditor *pEditor, const std::shared_ptr &pEnv); + + void Undo() override; + void Redo() override; + +private: + std::shared_ptr m_pEnv; +}; + +class CEditorActionEveloppeDelete : public IEditorAction +{ +public: + CEditorActionEveloppeDelete(CEditor *pEditor, int EnvelopeIndex); + + void Undo() override; + void Redo() override; + +private: + int m_EnvelopeIndex; + std::shared_ptr m_pEnv; +}; + +class CEditorActionEnvelopeEdit : public IEditorAction +{ +public: + enum class EEditType + { + SYNC, + ORDER + }; + + CEditorActionEnvelopeEdit(CEditor *pEditor, int EnvelopeIndex, EEditType EditType, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_EnvelopeIndex; + EEditType m_EditType; + int m_Previous; + int m_Current; + std::shared_ptr m_pEnv; +}; + +class CEditorActionEnvelopeEditPoint : public IEditorAction +{ +public: + enum class EEditType + { + TIME, + VALUE, + CURVE_TYPE, + HANDLE + }; + + CEditorActionEnvelopeEditPoint(CEditor *pEditor, int EnvelopeIndex, int PointIndex, int Channel, EEditType EditType, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_EnvelopeIndex; + int m_PointIndex; + int m_Channel; + EEditType m_EditType; + int m_Previous; + int m_Current; + std::shared_ptr m_pEnv; + + void Apply(int Value); +}; + +class CEditorActionAddEnvelopePoint : public IEditorAction +{ +public: + CEditorActionAddEnvelopePoint(CEditor *pEditor, int EnvIndex, int Time, ColorRGBA Channels); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_Time; + ColorRGBA m_Channels; +}; + +class CEditorActionDeleteEnvelopePoint : public IEditorAction +{ +public: + CEditorActionDeleteEnvelopePoint(CEditor *pEditor, int EnvIndex, int PointIndex); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_PointIndex; + CEnvPoint_runtime m_Point; +}; + +class CEditorActionEditEnvelopePointValue : public IEditorAction +{ +public: + enum class EType + { + TANGENT_IN, + TANGENT_OUT, + POINT + }; + + CEditorActionEditEnvelopePointValue(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, EType Type, int OldTime, int OldValue, int NewTime, int NewValue); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_PtIndex; + int m_Channel; + EType m_Type; + int m_OldTime; + int m_OldValue; + int m_NewTime; + int m_NewValue; + + void Apply(bool Undo); +}; + +class CEditorActionResetEnvelopePointTangent : public IEditorAction +{ +public: + CEditorActionResetEnvelopePointTangent(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, bool In); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_PointIndex; + int m_Channel; + bool m_In; + int m_Previous[2]; +}; + +class CEditorActionEditLayerSoundsProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerSoundsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerSoundsProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + void Apply(int Value); +}; + +class CEditorActionDeleteSoundSource : public CEditorActionLayerBase +{ +public: + CEditorActionDeleteSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + CSoundSource m_Source; +}; + +class CEditorActionEditSoundSource : public CEditorActionLayerBase +{ +public: + enum class EEditType + { + SHAPE + }; + + CEditorActionEditSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, EEditType Type, int Value); + ~CEditorActionEditSoundSource() override; + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + EEditType m_EditType; + int m_CurrentValue; + + std::vector m_vOriginalValues; + void *m_pSavedObject; + + void Save(); +}; + +class CEditorActionEditSoundSourceProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditSoundSourceProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ESoundProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + +private: + void Apply(int Value); +}; + +class CEditorActionEditRectSoundSourceShapeProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditRectSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ERectangleShapeProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + +private: + void Apply(int Value); +}; + +class CEditorActionEditCircleSoundSourceShapeProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditCircleSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ECircleShapeProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + +private: + void Apply(int Value); +}; + +class CEditorActionNewEmptySound : public CEditorActionLayerBase +{ +public: + CEditorActionNewEmptySound(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y); + + void Undo() override; + void Redo() override; + +private: + int m_X; + int m_Y; +}; + +class CEditorActionNewEmptyQuad : public CEditorActionLayerBase +{ +public: + CEditorActionNewEmptyQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y); + + void Undo() override; + void Redo() override; + +private: + int m_X; + int m_Y; +}; + +class CEditorActionNewQuad : public CEditorActionLayerBase +{ +public: + CEditorActionNewQuad(CEditor *pEditor, int GroupIndex, int LayerIndex); + + void Undo() override; + void Redo() override; + +private: + CQuad m_Quad; +}; + +#endif diff --git a/src/game/editor/editor_history.cpp b/src/game/editor/editor_history.cpp new file mode 100644 index 000000000..46110de2d --- /dev/null +++ b/src/game/editor/editor_history.cpp @@ -0,0 +1,65 @@ +#include + +#include "editor.h" +#include "editor_actions.h" +#include "editor_history.h" + +void CEditorHistory::RecordAction(const std::shared_ptr &pAction) +{ + RecordAction(pAction, nullptr); +} + +void CEditorHistory::Execute(const std::shared_ptr &pAction, const char *pDisplay) +{ + pAction->Redo(); + RecordAction(pAction, pDisplay); +} + +void CEditorHistory::RecordAction(const std::shared_ptr &pAction, const char *pDisplay) +{ + m_vpRedoActions.clear(); + + if((int)m_vpUndoActions.size() >= g_Config.m_ClEditorMaxHistory) + { + m_vpUndoActions.pop_front(); + } + + if(pDisplay == nullptr) + m_vpUndoActions.emplace_back(pAction); + else + m_vpUndoActions.emplace_back(std::make_shared(m_pEditor, std::vector>{pAction}, pDisplay)); +} + +bool CEditorHistory::Undo() +{ + if(m_vpUndoActions.empty()) + return false; + + auto pLastAction = m_vpUndoActions.back(); + m_vpUndoActions.pop_back(); + + pLastAction->Undo(); + + m_vpRedoActions.emplace_back(pLastAction); + return true; +} + +bool CEditorHistory::Redo() +{ + if(m_vpRedoActions.empty()) + return false; + + auto pLastAction = m_vpRedoActions.back(); + m_vpRedoActions.pop_back(); + + pLastAction->Redo(); + + m_vpUndoActions.emplace_back(pLastAction); + return true; +} + +void CEditorHistory::Clear() +{ + m_vpUndoActions.clear(); + m_vpRedoActions.clear(); +} diff --git a/src/game/editor/editor_history.h b/src/game/editor/editor_history.h new file mode 100644 index 000000000..144147140 --- /dev/null +++ b/src/game/editor/editor_history.h @@ -0,0 +1,37 @@ +#ifndef GAME_EDITOR_EDITOR_HISTORY_H +#define GAME_EDITOR_EDITOR_HISTORY_H + +#include "editor_action.h" + +#include + +class CEditorHistory +{ +public: + CEditorHistory() + { + m_pEditor = nullptr; + } + + ~CEditorHistory() + { + Clear(); + } + + void RecordAction(const std::shared_ptr &pAction); + void RecordAction(const std::shared_ptr &pAction, const char *pDisplay); + void Execute(const std::shared_ptr &pAction, const char *pDisplay = nullptr); + + bool Undo(); + bool Redo(); + + void Clear(); + bool CanUndo() const { return !m_vpUndoActions.empty(); } + bool CanRedo() const { return !m_vpRedoActions.empty(); } + + CEditor *m_pEditor; + std::deque> m_vpUndoActions; + std::deque> m_vpRedoActions; +}; + +#endif diff --git a/src/game/editor/editor_props.cpp b/src/game/editor/editor_props.cpp new file mode 100644 index 000000000..7d57b9c8f --- /dev/null +++ b/src/game/editor/editor_props.cpp @@ -0,0 +1,282 @@ +#include "editor.h" + +#include +#include + +int CEditor::DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) +{ + auto Res = DoPropertiesWithState(pToolbox, pProps, pIDs, pNewVal, Color); + return Res.m_Value; +} + +template +SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) +{ + int Change = -1; + EEditState State = EEditState::EDITING; + + for(int i = 0; pProps[i].m_pName; i++) + { + CUIRect Slot; + pToolBox->HSplitTop(13.0f, &Slot, pToolBox); + CUIRect Label, Shifter; + Slot.VSplitMid(&Label, &Shifter); + Shifter.HMargin(1.0f, &Shifter); + UI()->DoLabel(&Label, pProps[i].m_pName, 10.0f, TEXTALIGN_ML); + + if(pProps[i].m_Type == PROPTYPE_INT_STEP) + { + CUIRect Inc, Dec; + char aBuf[64]; + + Shifter.VSplitRight(10.0f, &Shifter, &Inc); + Shifter.VSplitLeft(10.0f, &Dec, &Shifter); + str_from_int(pProps[i].m_Value, aBuf); + auto NewValueRes = UiDoValueSelector((char *)&pIDs[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0, &Color); + int NewValue = NewValueRes.m_Value; + if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) + { + *pNewVal = NewValue; + Change = i; + State = NewValueRes.m_State; + } + if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) + { + *pNewVal = pProps[i].m_Value - 1; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Increase")) + { + *pNewVal = pProps[i].m_Value + 1; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_BOOL) + { + CUIRect No, Yes; + Shifter.VSplitMid(&No, &Yes); + if(DoButton_ButtonDec(&pIDs[i], "No", !pProps[i].m_Value, &No, 0, "")) + { + *pNewVal = 0; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 1, "Yes", pProps[i].m_Value, &Yes, 0, "")) + { + *pNewVal = 1; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_INT_SCROLL) + { + auto NewValueRes = UiDoValueSelector(&pIDs[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text."); + int NewValue = NewValueRes.m_Value; + if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) + { + *pNewVal = NewValue; + Change = i; + State = NewValueRes.m_State; + } + } + else if(pProps[i].m_Type == PROPTYPE_ANGLE_SCROLL) + { + CUIRect Inc, Dec; + Shifter.VSplitRight(10.0f, &Shifter, &Inc); + Shifter.VSplitLeft(10.0f, &Dec, &Shifter); + const bool Shift = Input()->ShiftIsPressed(); + int Step = Shift ? 1 : 45; + int Value = pProps[i].m_Value; + + auto NewValueRes = UiDoValueSelector(&pIDs[i], &Shifter, "", Value, pProps[i].m_Min, pProps[i].m_Max, Shift ? 1 : 45, Shift ? 1.0f : 10.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0); + int NewValue = NewValueRes.m_Value; + if(DoButton_ButtonDec(&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) + { + NewValue = (std::ceil((pProps[i].m_Value / (float)Step)) - 1) * Step; + if(NewValue < 0) + NewValue += 360; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(&pIDs[i] + 2, nullptr, 0, &Inc, 0, "Increase")) + { + NewValue = (pProps[i].m_Value + Step) / Step * Step; + State = EEditState::ONE_GO; + } + + if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) + { + *pNewVal = NewValue % 360; + Change = i; + State = NewValueRes.m_State; + } + } + else if(pProps[i].m_Type == PROPTYPE_COLOR) + { + const auto &&SetColor = [&](ColorRGBA NewColor) { + const int NewValue = NewColor.PackAlphaLast(); + if(NewValue != pProps[i].m_Value || m_ColorPickerPopupContext.m_State != EEditState::EDITING) + { + *pNewVal = NewValue; + Change = i; + State = m_ColorPickerPopupContext.m_State; + } + }; + DoColorPickerButton(&pIDs[i], &Shifter, ColorRGBA::UnpackAlphaLast(pProps[i].m_Value), SetColor); + } + else if(pProps[i].m_Type == PROPTYPE_IMAGE) + { + const char *pName; + if(pProps[i].m_Value < 0) + pName = "None"; + else + pName = m_Map.m_vpImages[pProps[i].m_Value]->m_aName; + + if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) + PopupSelectImageInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + + int r = PopupSelectImageResult(); + if(r >= -1) + { + *pNewVal = r; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_SHIFT) + { + CUIRect Left, Right, Up, Down; + Shifter.VSplitMid(&Left, &Up, 2.0f); + Left.VSplitLeft(10.0f, &Left, &Shifter); + Shifter.VSplitRight(10.0f, &Shifter, &Right); + Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), 0, 0.0f); + UI()->DoLabel(&Shifter, "X", 10.0f, TEXTALIGN_MC); + Up.VSplitLeft(10.0f, &Up, &Shifter); + Shifter.VSplitRight(10.0f, &Shifter, &Down); + Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), 0, 0.0f); + UI()->DoLabel(&Shifter, "Y", 10.0f, TEXTALIGN_MC); + if(DoButton_ButtonDec(&pIDs[i], "-", 0, &Left, 0, "Left")) + { + *pNewVal = DIRECTION_LEFT; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 3, "+", 0, &Right, 0, "Right")) + { + *pNewVal = DIRECTION_RIGHT; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonDec(((char *)&pIDs[i]) + 1, "-", 0, &Up, 0, "Up")) + { + *pNewVal = DIRECTION_UP; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, "+", 0, &Down, 0, "Down")) + { + *pNewVal = DIRECTION_DOWN; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_SOUND) + { + const char *pName; + if(pProps[i].m_Value < 0) + pName = "None"; + else + pName = m_Map.m_vpSounds[pProps[i].m_Value]->m_aName; + + if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) + PopupSelectSoundInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + + int r = PopupSelectSoundResult(); + if(r >= -1) + { + *pNewVal = r; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_AUTOMAPPER) + { + const char *pName; + if(pProps[i].m_Value < 0 || pProps[i].m_Min < 0 || pProps[i].m_Min >= (int)m_Map.m_vpImages.size()) + pName = "None"; + else + pName = m_Map.m_vpImages[pProps[i].m_Min]->m_AutoMapper.GetConfigName(pProps[i].m_Value); + + if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) + PopupSelectConfigAutoMapInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + + int r = PopupSelectConfigAutoMapResult(); + if(r >= -1) + { + *pNewVal = r; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_ENVELOPE) + { + CUIRect Inc, Dec; + char aBuf[8]; + int CurValue = pProps[i].m_Value; + + Shifter.VSplitRight(10.0f, &Shifter, &Inc); + Shifter.VSplitLeft(10.0f, &Dec, &Shifter); + + if(CurValue <= 0) + str_copy(aBuf, "None:"); + else if(m_Map.m_vpEnvelopes[CurValue - 1]->m_aName[0]) + { + str_format(aBuf, sizeof(aBuf), "%s:", m_Map.m_vpEnvelopes[CurValue - 1]->m_aName); + if(!str_endswith(aBuf, ":")) + { + aBuf[sizeof(aBuf) - 2] = ':'; + aBuf[sizeof(aBuf) - 1] = '\0'; + } + } + else + aBuf[0] = '\0'; + + auto NewValueRes = UiDoValueSelector((char *)&pIDs[i], &Shifter, aBuf, CurValue, 0, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Set Envelope", false, false, IGraphics::CORNER_NONE); + int NewVal = NewValueRes.m_Value; + if(NewVal != CurValue || NewValueRes.m_State != EEditState::EDITING) + { + *pNewVal = NewVal; + Change = i; + State = NewValueRes.m_State; + } + + if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Previous Envelope")) + { + *pNewVal = pProps[i].m_Value - 1; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Next Envelope")) + { + *pNewVal = pProps[i].m_Value + 1; + Change = i; + State = EEditState::ONE_GO; + } + } + } + + return SEditResult{State, static_cast(Change)}; +} + +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); diff --git a/src/game/editor/editor_trackers.cpp b/src/game/editor/editor_trackers.cpp new file mode 100644 index 000000000..8f3660422 --- /dev/null +++ b/src/game/editor/editor_trackers.cpp @@ -0,0 +1,632 @@ +#include "editor_trackers.h" + +#include +#include + +#include "editor.h" +#include "editor_actions.h" + +CQuadEditTracker::CQuadEditTracker() : + m_pEditor(nullptr), m_TrackedProp(EQuadProp::PROP_NONE) {} + +CQuadEditTracker::~CQuadEditTracker() +{ + m_InitalPoints.clear(); + m_vSelectedQuads.clear(); +} + +void CQuadEditTracker::BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads) +{ + if(m_Tracking) + return; + m_Tracking = true; + m_vSelectedQuads.clear(); + m_pLayer = pLayer; + // Init all points + for(auto QuadIndex : vSelectedQuads) + { + auto &pQuad = pLayer->m_vQuads[QuadIndex]; + m_InitalPoints[QuadIndex] = std::vector(pQuad.m_aPoints, pQuad.m_aPoints + 5); + m_vSelectedQuads.push_back(QuadIndex); + } +} + +void CQuadEditTracker::EndQuadTrack() +{ + if(!m_Tracking) + return; + m_Tracking = false; + + int GroupIndex = m_pEditor->m_SelectedGroup; + int LayerIndex = m_pEditor->m_vSelectedLayers[0]; + + // Record all moved stuff + std::vector> vpActions; + for(auto QuadIndex : m_vSelectedQuads) + { + auto &pQuad = m_pLayer->m_vQuads[QuadIndex]; + auto vCurrentPoints = std::vector(pQuad.m_aPoints, pQuad.m_aPoints + 5); + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + } + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); +} + +void CQuadEditTracker::BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop) +{ + if(m_TrackedProp != EQuadProp::PROP_NONE) + return; + m_TrackedProp = Prop; + m_pLayer = pLayer; + m_vSelectedQuads = vSelectedQuads; + m_PreviousValues.clear(); + + for(auto QuadIndex : vSelectedQuads) + { + auto &Quad = pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadProp::PROP_POS_X || Prop == EQuadProp::PROP_POS_Y) + m_InitalPoints[QuadIndex] = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + else if(Prop == EQuadProp::PROP_POS_ENV) + m_PreviousValues[QuadIndex] = Quad.m_PosEnv; + else if(Prop == EQuadProp::PROP_POS_ENV_OFFSET) + m_PreviousValues[QuadIndex] = Quad.m_PosEnvOffset; + else if(Prop == EQuadProp::PROP_COLOR_ENV) + m_PreviousValues[QuadIndex] = Quad.m_ColorEnv; + else if(Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) + m_PreviousValues[QuadIndex] = Quad.m_ColorEnvOffset; + } +} +void CQuadEditTracker::EndQuadPropTrack(EQuadProp Prop) +{ + if(m_TrackedProp != Prop) + return; + m_TrackedProp = EQuadProp::PROP_NONE; + + std::vector> vpActions; + + int GroupIndex = m_pEditor->m_SelectedGroup; + int LayerIndex = m_pEditor->m_vSelectedLayers[0]; + + for(auto QuadIndex : m_vSelectedQuads) + { + auto &Quad = m_pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadProp::PROP_POS_X || Prop == EQuadProp::PROP_POS_Y) + { + auto vCurrentPoints = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + } + else + { + int Value = 0; + if(Prop == EQuadProp::PROP_POS_ENV) + Value = Quad.m_PosEnv; + else if(Prop == EQuadProp::PROP_POS_ENV_OFFSET) + Value = Quad.m_PosEnvOffset; + else if(Prop == EQuadProp::PROP_COLOR_ENV) + Value = Quad.m_ColorEnv; + else if(Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) + Value = Quad.m_ColorEnvOffset; + + if(Value != m_PreviousValues[QuadIndex]) + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, Prop, m_PreviousValues[QuadIndex], Value)); + } + } + + if(!vpActions.empty()) + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); +} + +void CQuadEditTracker::BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints) +{ + if(!m_vTrackedProps.empty()) + return; + + m_pLayer = pLayer; + m_SelectedQuadPoints = SelectedQuadPoints; + m_vSelectedQuads = vSelectedQuads; + m_PreviousValuesPoint.clear(); + + for(auto QuadIndex : vSelectedQuads) + { + m_PreviousValuesPoint[QuadIndex] = std::vector>(4); + } +} + +void CQuadEditTracker::AddQuadPointPropTrack(EQuadPointProp Prop) +{ + if(std::find(m_vTrackedProps.begin(), m_vTrackedProps.end(), Prop) != m_vTrackedProps.end()) + return; + + m_vTrackedProps.push_back(Prop); + + for(auto QuadIndex : m_vSelectedQuads) + { + auto &Quad = m_pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadPointProp::PROP_POS_X || Prop == EQuadPointProp::PROP_POS_Y) + m_InitalPoints[QuadIndex] = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + else if(Prop == EQuadPointProp::PROP_COLOR) + { + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + int Color = PackColor(Quad.m_aColors[v]); + m_PreviousValuesPoint[QuadIndex][v][Prop] = Color; + } + } + } + else if(Prop == EQuadPointProp::PROP_TEX_U) + { + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + m_PreviousValuesPoint[QuadIndex][v][Prop] = Quad.m_aTexcoords[v].x; + } + } + } + else if(Prop == EQuadPointProp::PROP_TEX_V) + { + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + m_PreviousValuesPoint[QuadIndex][v][Prop] = Quad.m_aTexcoords[v].y; + } + } + } + } +} + +void CQuadEditTracker::EndQuadPointPropTrack(EQuadPointProp Prop) +{ + auto It = std::find(m_vTrackedProps.begin(), m_vTrackedProps.end(), Prop); + if(It == m_vTrackedProps.end()) + return; + + m_vTrackedProps.erase(It); + + std::vector> vpActions; + + int GroupIndex = m_pEditor->m_SelectedGroup; + int LayerIndex = m_pEditor->m_vSelectedLayers[0]; + + for(auto QuadIndex : m_vSelectedQuads) + { + auto &Quad = m_pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadPointProp::PROP_POS_X || Prop == EQuadPointProp::PROP_POS_Y) + { + auto vCurrentPoints = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + } + else + { + int Value = 0; + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + if(Prop == EQuadPointProp::PROP_COLOR) + { + Value = PackColor(Quad.m_aColors[v]); + } + else if(Prop == EQuadPointProp::PROP_TEX_U) + { + Value = Quad.m_aTexcoords[v].x; + } + else if(Prop == EQuadPointProp::PROP_TEX_V) + { + Value = Quad.m_aTexcoords[v].y; + } + + if(Value != m_PreviousValuesPoint[QuadIndex][v][Prop]) + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, v, Prop, m_PreviousValuesPoint[QuadIndex][v][Prop], Value)); + } + } + } + } + + if(!vpActions.empty()) + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); +} + +void CQuadEditTracker::EndQuadPointPropTrackAll() +{ + std::vector> vpActions; + for(auto &Prop : m_vTrackedProps) + { + int GroupIndex = m_pEditor->m_SelectedGroup; + int LayerIndex = m_pEditor->m_vSelectedLayers[0]; + + for(auto QuadIndex : m_vSelectedQuads) + { + auto &Quad = m_pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadPointProp::PROP_POS_X || Prop == EQuadPointProp::PROP_POS_Y) + { + auto vCurrentPoints = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + } + else + { + int Value = 0; + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + if(Prop == EQuadPointProp::PROP_COLOR) + { + Value = PackColor(Quad.m_aColors[v]); + } + else if(Prop == EQuadPointProp::PROP_TEX_U) + { + Value = Quad.m_aTexcoords[v].x; + } + else if(Prop == EQuadPointProp::PROP_TEX_V) + { + Value = Quad.m_aTexcoords[v].y; + } + + if(Value != m_PreviousValuesPoint[QuadIndex][v][Prop]) + vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, v, Prop, m_PreviousValuesPoint[QuadIndex][v][Prop], Value)); + } + } + } + } + } + + if(!vpActions.empty()) + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); + + m_vTrackedProps.clear(); +} + +void CEditorPropTracker::BeginPropTrack(int Prop, int Value) +{ + if(m_TrackedProp != -1 || m_TrackedProp == Prop) + return; + m_TrackedProp = Prop; + m_PreviousValue = Value; +} + +void CEditorPropTracker::StopPropTrack(int Prop, int Value) +{ + if(Prop != m_TrackedProp) + return; + m_TrackedProp = -1; + m_CurrentValue = Value; +} + +// ----------------------------- + +void CEnvelopeEditorOperationTracker::Begin(EEnvelopeEditorOp Operation) +{ + if(m_TrackedOp == Operation) + return; + + if(m_TrackedOp != EEnvelopeEditorOp::OP_NONE) + { + Stop(true); + } + + m_TrackedOp = Operation; + + if(Operation == EEnvelopeEditorOp::OP_DRAG_POINT || Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X || Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y || Operation == EEnvelopeEditorOp::OP_SCALE) + { + HandlePointDragStart(); + } +} + +void CEnvelopeEditorOperationTracker::Stop(bool Switch) +{ + if(m_TrackedOp == EEnvelopeEditorOp::OP_NONE) + return; + + if(m_TrackedOp == EEnvelopeEditorOp::OP_DRAG_POINT || m_TrackedOp == EEnvelopeEditorOp::OP_DRAG_POINT_X || m_TrackedOp == EEnvelopeEditorOp::OP_DRAG_POINT_Y || m_TrackedOp == EEnvelopeEditorOp::OP_SCALE) + { + HandlePointDragEnd(Switch); + } + + m_TrackedOp = EEnvelopeEditorOp::OP_NONE; +} + +void CEnvelopeEditorOperationTracker::HandlePointDragStart() +{ + // Figure out which points are selected and which channels + // Save their X and Y position (time and value) + auto pEnv = m_pEditor->m_Map.m_vpEnvelopes[m_pEditor->m_SelectedEnvelope]; + + for(auto [PointIndex, Channel] : m_pEditor->m_vSelectedEnvelopePoints) + { + auto &Point = pEnv->m_vPoints[PointIndex]; + auto &Data = m_SavedValues[PointIndex]; + Data.m_Values[Channel] = Point.m_aValues[Channel]; + if(Data.m_Used) + continue; + Data.m_Time = Point.m_Time; + Data.m_Used = true; + } +} + +void CEnvelopeEditorOperationTracker::HandlePointDragEnd(bool Switch) +{ + if(Switch && m_TrackedOp != EEnvelopeEditorOp::OP_SCALE) + return; + + int EnvIndex = m_pEditor->m_SelectedEnvelope; + auto pEnv = m_pEditor->m_Map.m_vpEnvelopes[EnvIndex]; + std::vector> vpActions; + + for(auto const &Entry : m_SavedValues) + { + int PointIndex = Entry.first; + auto &Point = pEnv->m_vPoints[PointIndex]; + const auto &Data = Entry.second; + + if(Data.m_Time != Point.m_Time) + { // Save time + vpActions.push_back(std::make_shared(m_pEditor, EnvIndex, PointIndex, 0, CEditorActionEnvelopeEditPoint::EEditType::TIME, Data.m_Time, Point.m_Time)); + } + + for(auto Value : Data.m_Values) + { + // Value.second is the saved value, Value.first is the channel + int Channel = Value.first; + if(Value.second != Point.m_aValues[Channel]) + { // Save value + vpActions.push_back(std::make_shared(m_pEditor, EnvIndex, PointIndex, Channel, CEditorActionEnvelopeEditPoint::EEditType::VALUE, Value.second, Point.m_aValues[Channel])); + } + } + } + + if(!vpActions.empty()) + { + m_pEditor->m_EnvelopeEditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, "Envelope point drag")); + } + + m_SavedValues.clear(); +} + +// ----------------------------------------------------------------------- + +void CLayerPropTracker::OnEnd(ELayerProp Prop, int Value) +{ + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value)); +} + +int CLayerPropTracker::PropToValue(ELayerProp Prop) +{ + switch(Prop) + { + case ELayerProp::PROP_GROUP: return m_pEditor->m_SelectedGroup; + case ELayerProp::PROP_HQ: return m_pObject->m_Flags; + case ELayerProp::PROP_ORDER: return m_pEditor->m_vSelectedLayers[0]; + default: return 0; + } +} + +// ----------------------------------------------------------------------- + +bool CLayerTilesPropTracker::EndChecker(ETilesProp Prop, EEditState State, int Value) +{ + return (State == EEditState::END || State == EEditState::ONE_GO) && (Value != m_OriginalValue || Prop == ETilesProp::PROP_SHIFT); +} + +void CLayerTilesPropTracker::OnStart(ETilesProp Prop) +{ + if(Prop == ETilesProp::PROP_WIDTH || Prop == ETilesProp::PROP_HEIGHT) + { + m_SavedLayers[LAYERTYPE_TILES] = m_pObject->Duplicate(); + if(m_pObject->m_Game || m_pObject->m_Front || m_pObject->m_Switch || m_pObject->m_Speedup || m_pObject->m_Tune || m_pObject->m_Tele) + { // Need to save all entities layers when any entity layer + if(m_pEditor->m_Map.m_pFrontLayer && !m_pObject->m_Front) + m_SavedLayers[LAYERTYPE_FRONT] = m_pEditor->m_Map.m_pFrontLayer->Duplicate(); + if(m_pEditor->m_Map.m_pTeleLayer && !m_pObject->m_Tele) + m_SavedLayers[LAYERTYPE_TELE] = m_pEditor->m_Map.m_pTeleLayer->Duplicate(); + if(m_pEditor->m_Map.m_pSwitchLayer && !m_pObject->m_Switch) + m_SavedLayers[LAYERTYPE_SWITCH] = m_pEditor->m_Map.m_pSwitchLayer->Duplicate(); + if(m_pEditor->m_Map.m_pSpeedupLayer && !m_pObject->m_Speedup) + m_SavedLayers[LAYERTYPE_SPEEDUP] = m_pEditor->m_Map.m_pSpeedupLayer->Duplicate(); + if(m_pEditor->m_Map.m_pTuneLayer && !m_pObject->m_Tune) + m_SavedLayers[LAYERTYPE_TUNE] = m_pEditor->m_Map.m_pTuneLayer->Duplicate(); + if(!m_pObject->m_Game) + m_SavedLayers[LAYERTYPE_GAME] = m_pEditor->m_Map.m_pGameLayer->Duplicate(); + } + } + else if(Prop == ETilesProp::PROP_SHIFT) + { + m_SavedLayers[LAYERTYPE_TILES] = m_pObject->Duplicate(); + } +} + +void CLayerTilesPropTracker::OnEnd(ETilesProp Prop, int Value) +{ + auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value); + + pAction->SetSavedLayers(m_SavedLayers); + m_SavedLayers.clear(); + + m_pEditor->m_EditorHistory.RecordAction(pAction); +} + +int CLayerTilesPropTracker::PropToValue(ETilesProp Prop) +{ + switch(Prop) + { + case ETilesProp::PROP_AUTOMAPPER: return m_pObject->m_AutoMapperConfig; + case ETilesProp::PROP_COLOR: return PackColor(m_pObject->m_Color); + case ETilesProp::PROP_COLOR_ENV: return m_pObject->m_ColorEnv; + case ETilesProp::PROP_COLOR_ENV_OFFSET: return m_pObject->m_ColorEnvOffset; + case ETilesProp::PROP_HEIGHT: return m_pObject->m_Height; + case ETilesProp::PROP_WIDTH: return m_pObject->m_Width; + case ETilesProp::PROP_IMAGE: return m_pObject->m_Image; + case ETilesProp::PROP_SEED: return m_pObject->m_Seed; + case ETilesProp::PROP_SHIFT_BY: return m_pEditor->m_ShiftBy; + default: return 0; + } +} + +// ------------------------------ + +void CLayerTilesCommonPropTracker::OnStart(ETilesCommonProp Prop) +{ + for(auto &pLayer : m_vpLayers) + { + if(Prop == ETilesCommonProp::PROP_SHIFT) + { + m_SavedLayers[pLayer][LAYERTYPE_TILES] = pLayer->Duplicate(); + } + } +} + +void CLayerTilesCommonPropTracker::OnEnd(ETilesCommonProp Prop, int Value) +{ + std::vector> vpActions; + + static std::map s_PropMap{ + {ETilesCommonProp::PROP_COLOR, ETilesProp::PROP_COLOR}, + {ETilesCommonProp::PROP_SHIFT, ETilesProp::PROP_SHIFT}, + {ETilesCommonProp::PROP_SHIFT_BY, ETilesProp::PROP_SHIFT_BY}}; + + int j = 0; + for(auto &pLayer : m_vpLayers) + { + int LayerIndex = m_vLayerIndices[j++]; + auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, LayerIndex, s_PropMap[Prop], m_OriginalValue, Value); + pAction->SetSavedLayers(m_SavedLayers[pLayer]); + vpActions.push_back(pAction); + } + + char aDisplay[256]; + static const char *s_apNames[] = { + "width", + "height", + "shift", + "shift by", + "color", + }; + + str_format(aDisplay, sizeof(aDisplay), "Edit %d layers common property: %s", (int)m_vpLayers.size(), s_apNames[(int)Prop]); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay)); +} + +bool CLayerTilesCommonPropTracker::EndChecker(ETilesCommonProp Prop, EEditState State, int Value) +{ + return (State == EEditState::END || State == EEditState::ONE_GO) && (Value != m_OriginalValue || Prop == ETilesCommonProp::PROP_SHIFT); +} + +int CLayerTilesCommonPropTracker::PropToValue(ETilesCommonProp Prop) +{ + if(Prop == ETilesCommonProp::PROP_SHIFT_BY) + return m_pEditor->m_ShiftBy; + return 0; +} + +// ------------------------------ + +void CLayerGroupPropTracker::OnEnd(EGroupProp Prop, int Value) +{ + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, Prop, m_OriginalValue, Value)); +} + +int CLayerGroupPropTracker::PropToValue(EGroupProp Prop) +{ + switch(Prop) + { + case EGroupProp::PROP_ORDER: return m_pEditor->m_SelectedGroup; + case EGroupProp::PROP_POS_X: return m_pObject->m_OffsetX; + case EGroupProp::PROP_POS_Y: return m_pObject->m_OffsetY; + case EGroupProp::PROP_PARA_X: return m_pObject->m_ParallaxX; + case EGroupProp::PROP_PARA_Y: return m_pObject->m_ParallaxY; + case EGroupProp::PROP_USE_CLIPPING: return m_pObject->m_UseClipping; + case EGroupProp::PROP_CLIP_X: return m_pObject->m_ClipX; + case EGroupProp::PROP_CLIP_Y: return m_pObject->m_ClipY; + case EGroupProp::PROP_CLIP_W: return m_pObject->m_ClipW; + case EGroupProp::PROP_CLIP_H: return m_pObject->m_ClipH; + default: return 0; + } +} + +// ------------------------------------------------------------------ + +void CLayerQuadsPropTracker::OnEnd(ELayerQuadsProp Prop, int Value) +{ + auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value); + m_pEditor->m_EditorHistory.RecordAction(pAction); +} + +int CLayerQuadsPropTracker::PropToValue(ELayerQuadsProp Prop) +{ + if(Prop == ELayerQuadsProp::PROP_IMAGE) + return m_pObject->m_Image; + return 0; +} + +// ------------------------------------------------------------------- + +void CLayerSoundsPropTracker::OnEnd(ELayerSoundsProp Prop, int Value) +{ + auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value); + m_pEditor->m_EditorHistory.RecordAction(pAction); +} + +int CLayerSoundsPropTracker::PropToValue(ELayerSoundsProp Prop) +{ + if(Prop == ELayerSoundsProp::PROP_SOUND) + return m_pObject->m_Sound; + return 0; +} + +// ---- + +void CSoundSourcePropTracker::OnEnd(ESoundProp Prop, int Value) +{ + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); +} + +int CSoundSourcePropTracker::PropToValue(ESoundProp Prop) +{ + switch(Prop) + { + case ESoundProp::PROP_POS_X: return m_pObject->m_Position.x; + case ESoundProp::PROP_POS_Y: return m_pObject->m_Position.y; + case ESoundProp::PROP_LOOP: return m_pObject->m_Loop; + case ESoundProp::PROP_PAN: return m_pObject->m_Pan; + case ESoundProp::PROP_TIME_DELAY: return m_pObject->m_TimeDelay; + case ESoundProp::PROP_FALLOFF: return m_pObject->m_Falloff; + case ESoundProp::PROP_POS_ENV: return m_pObject->m_PosEnv; + case ESoundProp::PROP_POS_ENV_OFFSET: return m_pObject->m_PosEnvOffset; + case ESoundProp::PROP_SOUND_ENV: return m_pObject->m_SoundEnv; + case ESoundProp::PROP_SOUND_ENV_OFFSET: return m_pObject->m_SoundEnvOffset; + default: return 0; + } +} + +// ---- + +void CSoundSourceRectShapePropTracker::OnEnd(ERectangleShapeProp Prop, int Value) +{ + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); +} + +int CSoundSourceRectShapePropTracker::PropToValue(ERectangleShapeProp Prop) +{ + switch(Prop) + { + case ERectangleShapeProp::PROP_RECTANGLE_WIDTH: return m_pObject->m_Shape.m_Rectangle.m_Width; + case ERectangleShapeProp::PROP_RECTANGLE_HEIGHT: return m_pObject->m_Shape.m_Rectangle.m_Height; + default: return 0; + } +} + +void CSoundSourceCircleShapePropTracker::OnEnd(ECircleShapeProp Prop, int Value) +{ + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); +} + +int CSoundSourceCircleShapePropTracker::PropToValue(ECircleShapeProp Prop) +{ + switch(Prop) + { + case ECircleShapeProp::PROP_CIRCLE_RADIUS: return m_pObject->m_Shape.m_Circle.m_Radius; + default: return 0; + } +} diff --git a/src/game/editor/editor_trackers.h b/src/game/editor/editor_trackers.h new file mode 100644 index 000000000..156096b6a --- /dev/null +++ b/src/game/editor/editor_trackers.h @@ -0,0 +1,271 @@ +#ifndef GAME_EDITOR_EDITOR_TRACKERS_H +#define GAME_EDITOR_EDITOR_TRACKERS_H + +#include +#include +#include + +#include +#include +#include + +class CEditor; +class CLayerTiles; +class CLayerGroup; +class CLayerSounds; +struct CSoundSource; + +class CQuadEditTracker +{ +public: + CQuadEditTracker(); + ~CQuadEditTracker(); + + void BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads); + void EndQuadTrack(); + + void BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop); + void EndQuadPropTrack(EQuadProp Prop); + + void BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints); + void AddQuadPointPropTrack(EQuadPointProp Prop); + void EndQuadPointPropTrack(EQuadPointProp Prop); + void EndQuadPointPropTrackAll(); + + CEditor *m_pEditor; + +private: + std::vector m_vSelectedQuads; + int m_SelectedQuadPoints; + std::map> m_InitalPoints; + + bool m_Tracking = false; + std::shared_ptr m_pLayer; + + EQuadProp m_TrackedProp; + std::vector m_vTrackedProps; + std::map m_PreviousValues; + std::map>> m_PreviousValuesPoint; +}; + +class CEditorPropTracker +{ +public: + CEditorPropTracker() = default; + + void BeginPropTrack(int Prop, int Value); + void StopPropTrack(int Prop, int Value); + inline void Reset() { m_TrackedProp = -1; } + + CEditor *m_pEditor; + int m_PreviousValue; + int m_CurrentValue; + +private: + int m_TrackedProp = -1; +}; + +enum class EEnvelopeEditorOp +{ + OP_NONE = 0, + OP_SELECT, + OP_DRAG_POINT, + OP_DRAG_POINT_X, + OP_DRAG_POINT_Y, + OP_CONTEXT_MENU, + OP_BOX_SELECT, + OP_SCALE +}; + +class CEnvelopeEditorOperationTracker +{ +public: + CEnvelopeEditorOperationTracker() = default; + + void Begin(EEnvelopeEditorOp Operation); + void Stop(bool Switch = true); + inline void Reset() { m_TrackedOp = EEnvelopeEditorOp::OP_NONE; } + + CEditor *m_pEditor; + +private: + EEnvelopeEditorOp m_TrackedOp = EEnvelopeEditorOp::OP_NONE; + + struct SPointData + { + bool m_Used; + int m_Time; + std::map m_Values; + }; + + std::map m_SavedValues; + + void HandlePointDragStart(); + void HandlePointDragEnd(bool Switch); +}; + +template +class CPropTracker +{ +public: + CPropTracker(CEditor *pEditor) : + m_pEditor(pEditor), m_OriginalValue(0) {} + CEditor *m_pEditor; + + void Begin(T *pObject, E Prop, EEditState State) + { + if(Prop == static_cast(-1)) + return; + m_pObject = pObject; + int Value = PropToValue(Prop); + if(StartChecker(Prop, State, Value)) + { + m_OriginalValue = Value; + OnStart(Prop); + } + } + + void End(E Prop, EEditState State) + { + if(Prop == static_cast(-1)) + return; + int Value = PropToValue(Prop); + if(EndChecker(Prop, State, Value)) + { + OnEnd(Prop, Value); + } + } + +protected: + virtual void OnStart(E Prop) {} + virtual void OnEnd(E Prop, int Value) {} + virtual int PropToValue(E Prop) { return 0; } + virtual bool StartChecker(E Prop, EEditState State, int Value) + { + return State == EEditState::START || State == EEditState::ONE_GO; + } + virtual bool EndChecker(E Prop, EEditState State, int Value) + { + return (State == EEditState::END || State == EEditState::ONE_GO) && (Value != m_OriginalValue); + } + + int m_OriginalValue; + T *m_pObject; +}; + +class CLayerPropTracker : public CPropTracker +{ +public: + CLayerPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(ELayerProp Prop, int Value) override; + int PropToValue(ELayerProp Prop) override; +}; + +class CLayerTilesPropTracker : public CPropTracker +{ +public: + CLayerTilesPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnStart(ETilesProp Prop) override; + void OnEnd(ETilesProp Prop, int Value) override; + bool EndChecker(ETilesProp Prop, EEditState State, int Value) override; + + int PropToValue(ETilesProp Prop) override; + +private: + std::map> m_SavedLayers; +}; + +class CLayerTilesCommonPropTracker : public CPropTracker +{ +public: + CLayerTilesCommonPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnStart(ETilesCommonProp Prop) override; + void OnEnd(ETilesCommonProp Prop, int Value) override; + bool EndChecker(ETilesCommonProp Prop, EEditState State, int Value) override; + + int PropToValue(ETilesCommonProp Prop) override; + +private: + std::map, std::map>> m_SavedLayers; + +public: + std::vector> m_vpLayers; + std::vector m_vLayerIndices; +}; + +class CLayerGroupPropTracker : public CPropTracker +{ +public: + CLayerGroupPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(EGroupProp Prop, int Value) override; + int PropToValue(EGroupProp Prop) override; +}; + +class CLayerQuadsPropTracker : public CPropTracker +{ +public: + CLayerQuadsPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(ELayerQuadsProp Prop, int Value) override; + int PropToValue(ELayerQuadsProp Prop) override; +}; + +class CLayerSoundsPropTracker : public CPropTracker +{ +public: + CLayerSoundsPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(ELayerSoundsProp Prop, int Value) override; + int PropToValue(ELayerSoundsProp Prop) override; +}; + +class CSoundSourcePropTracker : public CPropTracker +{ +public: + CSoundSourcePropTracker(CEditor *pEditor) : + CPropTracker(pEditor) {} + +protected: + void OnEnd(ESoundProp Prop, int Value) override; + int PropToValue(ESoundProp Prop) override; +}; + +class CSoundSourceRectShapePropTracker : public CPropTracker +{ +public: + CSoundSourceRectShapePropTracker(CEditor *pEditor) : + CPropTracker(pEditor) {} + +protected: + void OnEnd(ERectangleShapeProp Prop, int Value) override; + int PropToValue(ERectangleShapeProp Prop) override; +}; + +class CSoundSourceCircleShapePropTracker : public CPropTracker +{ +public: + CSoundSourceCircleShapePropTracker(CEditor *pEditor) : + CPropTracker(pEditor) {} + +protected: + void OnEnd(ECircleShapeProp Prop, int Value) override; + int PropToValue(ECircleShapeProp Prop) override; +}; + +#endif diff --git a/src/game/editor/mapitems.h b/src/game/editor/mapitems.h new file mode 100644 index 000000000..7873ee7e7 --- /dev/null +++ b/src/game/editor/mapitems.h @@ -0,0 +1,125 @@ +#ifndef GAME_EDITOR_MAPITEMS_H +#define GAME_EDITOR_MAPITEMS_H + +enum class EQuadProp +{ + PROP_NONE = -1, + PROP_ORDER, + PROP_POS_X, + PROP_POS_Y, + PROP_POS_ENV, + PROP_POS_ENV_OFFSET, + PROP_COLOR_ENV, + PROP_COLOR_ENV_OFFSET, + NUM_PROPS, +}; + +enum class EQuadPointProp +{ + PROP_NONE = -1, + PROP_POS_X, + PROP_POS_Y, + PROP_COLOR, + PROP_TEX_U, + PROP_TEX_V, + NUM_PROPS, +}; + +enum class ESoundProp +{ + PROP_NONE = -1, + PROP_POS_X, + PROP_POS_Y, + PROP_LOOP, + PROP_PAN, + PROP_TIME_DELAY, + PROP_FALLOFF, + PROP_POS_ENV, + PROP_POS_ENV_OFFSET, + PROP_SOUND_ENV, + PROP_SOUND_ENV_OFFSET, + NUM_PROPS, +}; + +enum class ERectangleShapeProp +{ + PROP_NONE = -1, + PROP_RECTANGLE_WIDTH, + PROP_RECTANGLE_HEIGHT, + NUM_RECTANGLE_PROPS, +}; + +enum class ECircleShapeProp +{ + PROP_NONE = -1, + PROP_CIRCLE_RADIUS, + NUM_CIRCLE_PROPS, +}; + +enum class ELayerProp +{ + PROP_NONE = -1, + PROP_GROUP, + PROP_ORDER, + PROP_HQ, + NUM_PROPS, +}; + +enum class ETilesProp +{ + PROP_NONE = -1, + PROP_WIDTH, + PROP_HEIGHT, + PROP_SHIFT, + PROP_SHIFT_BY, + PROP_IMAGE, + PROP_COLOR, + PROP_COLOR_ENV, + PROP_COLOR_ENV_OFFSET, + PROP_AUTOMAPPER, + PROP_SEED, + NUM_PROPS +}; + +enum class ETilesCommonProp +{ + PROP_NONE = -1, + PROP_WIDTH, + PROP_HEIGHT, + PROP_SHIFT, + PROP_SHIFT_BY, + PROP_COLOR, + NUM_PROPS, +}; + +enum class EGroupProp +{ + PROP_NONE = -1, + PROP_ORDER, + PROP_POS_X, + PROP_POS_Y, + PROP_PARA_X, + PROP_PARA_Y, + PROP_USE_CLIPPING, + PROP_CLIP_X, + PROP_CLIP_Y, + PROP_CLIP_W, + PROP_CLIP_H, + NUM_PROPS, +}; + +enum class ELayerQuadsProp +{ + PROP_NONE = -1, + PROP_IMAGE, + NUM_PROPS, +}; + +enum class ELayerSoundsProp +{ + PROP_NONE = -1, + PROP_SOUND, + NUM_PROPS, +}; + +#endif diff --git a/src/game/editor/mapitems/envelope.h b/src/game/editor/mapitems/envelope.h index 0d167e459..2b25c9f36 100644 --- a/src/game/editor/mapitems/envelope.h +++ b/src/game/editor/mapitems/envelope.h @@ -25,6 +25,7 @@ public: void AddPoint(int Time, int v0, int v1 = 0, int v2 = 0, int v3 = 0); float EndTime() const; int GetChannels() const; + EType Type() const { return m_Type; } private: void Resort(); diff --git a/src/game/editor/mapitems/layer.h b/src/game/editor/mapitems/layer.h index 298337d05..238c9e4cc 100644 --- a/src/game/editor/mapitems/layer.h +++ b/src/game/editor/mapitems/layer.h @@ -62,6 +62,7 @@ public: virtual void ModifySoundIndex(FIndexModifyFunction pfnFunc) {} virtual std::shared_ptr Duplicate() const = 0; + virtual const char *TypeName() const = 0; virtual void GetSize(float *pWidth, float *pHeight) { diff --git a/src/game/editor/mapitems/layer_front.cpp b/src/game/editor/mapitems/layer_front.cpp index 47d260586..f046299c4 100644 --- a/src/game/editor/mapitems/layer_front.cpp +++ b/src/game/editor/mapitems/layer_front.cpp @@ -45,3 +45,8 @@ void CLayerFront::Resize(int NewW, int NewH) if(m_pEditor->m_Map.m_pGameLayer->m_Width != NewW || m_pEditor->m_Map.m_pGameLayer->m_Height != NewH) m_pEditor->m_Map.m_pGameLayer->Resize(NewW, NewH); } + +const char *CLayerFront::TypeName() const +{ + return "front"; +} diff --git a/src/game/editor/mapitems/layer_front.h b/src/game/editor/mapitems/layer_front.h index f5aeb3a58..fbddfac1f 100644 --- a/src/game/editor/mapitems/layer_front.h +++ b/src/game/editor/mapitems/layer_front.h @@ -10,6 +10,7 @@ public: void Resize(int NewW, int NewH) override; void SetTile(int x, int y, CTile Tile) override; + const char *TypeName() const override; }; #endif diff --git a/src/game/editor/mapitems/layer_game.cpp b/src/game/editor/mapitems/layer_game.cpp index 48d061811..eb0ee541f 100644 --- a/src/game/editor/mapitems/layer_game.cpp +++ b/src/game/editor/mapitems/layer_game.cpp @@ -72,3 +72,8 @@ CUI::EPopupMenuFunctionResult CLayerGame::RenderProperties(CUIRect *pToolbox) m_Image = -1; return Result; } + +const char *CLayerGame::TypeName() const +{ + return "game"; +} diff --git a/src/game/editor/mapitems/layer_game.h b/src/game/editor/mapitems/layer_game.h index 4c00c7e7f..ec6f1d78f 100644 --- a/src/game/editor/mapitems/layer_game.h +++ b/src/game/editor/mapitems/layer_game.h @@ -11,6 +11,7 @@ public: CTile GetTile(int x, int y) override; void SetTile(int x, int y, CTile Tile) override; + const char *TypeName() const override; CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; }; diff --git a/src/game/editor/mapitems/layer_quads.cpp b/src/game/editor/mapitems/layer_quads.cpp index e1e58f3c9..3f69708fd 100644 --- a/src/game/editor/mapitems/layer_quads.cpp +++ b/src/game/editor/mapitems/layer_quads.cpp @@ -3,6 +3,7 @@ #include "layer_quads.h" #include +#include #include "image.h" @@ -140,6 +141,7 @@ int CLayerQuads::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) void CLayerQuads::BrushPlace(std::shared_ptr pBrush, float wx, float wy) { std::shared_ptr pQuadLayer = std::static_pointer_cast(pBrush); + std::vector vAddedQuads; for(const auto &Quad : pQuadLayer->m_vQuads) { CQuad n = Quad; @@ -151,7 +153,9 @@ void CLayerQuads::BrushPlace(std::shared_ptr pBrush, float wx, float wy) } m_vQuads.push_back(n); + vAddedQuads.push_back(n); } + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], vAddedQuads)); m_pEditor->m_Map.OnModify(); } @@ -219,26 +223,23 @@ void CLayerQuads::GetSize(float *pWidth, float *pHeight) CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) { - enum - { - PROP_IMAGE = 0, - NUM_PROPS, - }; - CProperty aProps[] = { {"Image", m_Image, PROPTYPE_IMAGE, -1, 0}, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ELayerQuadsProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); + if(Prop != ELayerQuadsProp::PROP_NONE) { m_pEditor->m_Map.OnModify(); } - if(Prop == PROP_IMAGE) + static CLayerQuadsPropTracker s_Tracker(m_pEditor); + s_Tracker.Begin(this, Prop, State); + + if(Prop == ELayerQuadsProp::PROP_IMAGE) { if(NewVal >= 0) m_Image = NewVal % m_pEditor->m_Map.m_vpImages.size(); @@ -246,6 +247,8 @@ CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) m_Image = -1; } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } @@ -280,3 +283,8 @@ int CLayerQuads::SwapQuads(int Index0, int Index1) std::swap(m_vQuads[Index0], m_vQuads[Index1]); return Index1; } + +const char *CLayerQuads::TypeName() const +{ + return "quads"; +} diff --git a/src/game/editor/mapitems/layer_quads.h b/src/game/editor/mapitems/layer_quads.h index 7ba890e68..dd826b385 100644 --- a/src/game/editor/mapitems/layer_quads.h +++ b/src/game/editor/mapitems/layer_quads.h @@ -28,6 +28,7 @@ public: void GetSize(float *pWidth, float *pHeight) override; std::shared_ptr Duplicate() const override; + const char *TypeName() const override; int m_Image; std::vector m_vQuads; diff --git a/src/game/editor/mapitems/layer_sounds.cpp b/src/game/editor/mapitems/layer_sounds.cpp index 5742368c0..ff8250617 100644 --- a/src/game/editor/mapitems/layer_sounds.cpp +++ b/src/game/editor/mapitems/layer_sounds.cpp @@ -1,7 +1,7 @@ #include "layer_sounds.h" #include - +#include #include static const float s_SourceVisualSize = 32.0f; @@ -171,6 +171,7 @@ int CLayerSounds::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy) { std::shared_ptr pSoundLayer = std::static_pointer_cast(pBrush); + std::vector vAddedSources; for(const auto &Source : pSoundLayer->m_vSources) { CSoundSource n = Source; @@ -179,32 +180,31 @@ void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy n.m_Position.y += f2fx(wy); m_vSources.push_back(n); + vAddedSources.push_back(n); } + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], vAddedSources)); m_pEditor->m_Map.OnModify(); } CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) { - enum - { - PROP_SOUND = 0, - NUM_PROPS, - }; - CProperty aProps[] = { {"Sound", m_Sound, PROPTYPE_SOUND, -1, 0}, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ELayerSoundsProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); + if(Prop != ELayerSoundsProp::PROP_NONE) { m_pEditor->m_Map.OnModify(); } - if(Prop == PROP_SOUND) + static CLayerSoundsPropTracker s_Tracker(m_pEditor); + s_Tracker.Begin(this, Prop, State); + + if(Prop == ELayerSoundsProp::PROP_SOUND) { if(NewVal >= 0) m_Sound = NewVal % m_pEditor->m_Map.m_vpSounds.size(); @@ -212,6 +212,8 @@ CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) m_Sound = -1; } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } @@ -233,3 +235,8 @@ std::shared_ptr CLayerSounds::Duplicate() const { return std::make_shared(*this); } + +const char *CLayerSounds::TypeName() const +{ + return "sounds"; +} diff --git a/src/game/editor/mapitems/layer_sounds.h b/src/game/editor/mapitems/layer_sounds.h index 0d89329f6..bc71b1364 100644 --- a/src/game/editor/mapitems/layer_sounds.h +++ b/src/game/editor/mapitems/layer_sounds.h @@ -23,6 +23,7 @@ public: void ModifySoundIndex(FIndexModifyFunction pfnFunc) override; std::shared_ptr Duplicate() const override; + const char *TypeName() const override; int m_Sound; std::vector m_vSources; diff --git a/src/game/editor/mapitems/layer_speedup.cpp b/src/game/editor/mapitems/layer_speedup.cpp index bcdd43d06..02795a431 100644 --- a/src/game/editor/mapitems/layer_speedup.cpp +++ b/src/game/editor/mapitems/layer_speedup.cpp @@ -12,6 +12,16 @@ CLayerSpeedup::CLayerSpeedup(CEditor *pEditor, int w, int h) : mem_zero(m_pSpeedupTile, (size_t)w * h * sizeof(CSpeedupTile)); } +CLayerSpeedup::CLayerSpeedup(const CLayerSpeedup &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Speedup copy"); + m_Speedup = 1; + + m_pSpeedupTile = new CSpeedupTile[m_Width * m_Height]; + mem_copy(m_pSpeedupTile, Other.m_pSpeedupTile, (size_t)m_Width * m_Height * sizeof(CSpeedupTile)); +} + CLayerSpeedup::~CLayerSpeedup() { delete[] m_pSpeedupTile; @@ -84,53 +94,78 @@ void CLayerSpeedup::BrushDraw(std::shared_ptr pBrush, float wx, float wy if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + SSpeedupTileStateChange::SData Previous{ + m_pSpeedupTile[Index].m_Force, + m_pSpeedupTile[Index].m_Angle, + m_pSpeedupTile[Index].m_MaxSpeed, + m_pSpeedupTile[Index].m_Type, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidSpeedupTile(pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index)) && pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index != TILE_AIR) { if(m_pEditor->m_SpeedupAngle != pSpeedupLayer->m_SpeedupAngle || m_pEditor->m_SpeedupForce != pSpeedupLayer->m_SpeedupForce || m_pEditor->m_SpeedupMaxSpeed != pSpeedupLayer->m_SpeedupMaxSpeed) { - m_pSpeedupTile[fy * m_Width + fx].m_Force = m_pEditor->m_SpeedupForce; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = m_pEditor->m_SpeedupAngle; - m_pSpeedupTile[fy * m_Width + fx].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pSpeedupTile[Index].m_Force = m_pEditor->m_SpeedupForce; + m_pSpeedupTile[Index].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; + m_pSpeedupTile[Index].m_Angle = m_pEditor->m_SpeedupAngle; + m_pSpeedupTile[Index].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; } else if(pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Force) { - m_pSpeedupTile[fy * m_Width + fx].m_Force = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Force; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Angle; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_MaxSpeed; - m_pSpeedupTile[fy * m_Width + fx].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pSpeedupTile[Index].m_Force = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Force; + m_pSpeedupTile[Index].m_Angle = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Angle; + m_pSpeedupTile[Index].m_MaxSpeed = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_MaxSpeed; + m_pSpeedupTile[Index].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; } else if(m_pEditor->m_SpeedupForce) { - m_pSpeedupTile[fy * m_Width + fx].m_Force = m_pEditor->m_SpeedupForce; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = m_pEditor->m_SpeedupAngle; - m_pSpeedupTile[fy * m_Width + fx].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pSpeedupTile[Index].m_Force = m_pEditor->m_SpeedupForce; + m_pSpeedupTile[Index].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; + m_pSpeedupTile[Index].m_Angle = m_pEditor->m_SpeedupAngle; + m_pSpeedupTile[Index].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; } else { - m_pSpeedupTile[fy * m_Width + fx].m_Force = 0; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pSpeedupTile[Index].m_Force = 0; + m_pSpeedupTile[Index].m_MaxSpeed = 0; + m_pSpeedupTile[Index].m_Angle = 0; + m_pSpeedupTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } } else { - m_pSpeedupTile[fy * m_Width + fx].m_Force = 0; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pSpeedupTile[Index].m_Force = 0; + m_pSpeedupTile[Index].m_MaxSpeed = 0; + m_pSpeedupTile[Index].m_Angle = 0; + m_pSpeedupTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } + + SSpeedupTileStateChange::SData Current{ + m_pSpeedupTile[Index].m_Force, + m_pSpeedupTile[Index].m_Angle, + m_pSpeedupTile[Index].m_MaxSpeed, + m_pSpeedupTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pSpeedupLayer->m_Width, pSpeedupLayer->m_Height); } +void CLayerSpeedup::RecordStateChange(int x, int y, SSpeedupTileStateChange::SData Previous, SSpeedupTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = SSpeedupTileStateChange{true, Previous, Current}; + else + m_History[y][x].m_Current = Current; +} + void CLayerSpeedup::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -211,6 +246,13 @@ void CLayerSpeedup::FillSelection(bool Empty, std::shared_ptr pBrush, CU const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + SSpeedupTileStateChange::SData Previous{ + m_pSpeedupTile[TgtIndex].m_Force, + m_pSpeedupTile[TgtIndex].m_Angle, + m_pSpeedupTile[TgtIndex].m_MaxSpeed, + m_pSpeedupTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidSpeedupTile((pLt->m_pTiles[SrcIndex]).m_Index))) // no speed up tile chosen: reset { m_pTiles[TgtIndex].m_Index = 0; @@ -240,7 +282,26 @@ void CLayerSpeedup::FillSelection(bool Empty, std::shared_ptr pBrush, CU m_pSpeedupTile[TgtIndex].m_MaxSpeed = pLt->m_pSpeedupTile[SrcIndex].m_MaxSpeed; } } + + SSpeedupTileStateChange::SData Current{ + m_pSpeedupTile[TgtIndex].m_Force, + m_pSpeedupTile[TgtIndex].m_Angle, + m_pSpeedupTile[TgtIndex].m_MaxSpeed, + m_pSpeedupTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); } + +std::shared_ptr CLayerSpeedup::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerSpeedup::TypeName() const +{ + return "speedup"; +} diff --git a/src/game/editor/mapitems/layer_speedup.h b/src/game/editor/mapitems/layer_speedup.h index 74d0a14c5..990f488e7 100644 --- a/src/game/editor/mapitems/layer_speedup.h +++ b/src/game/editor/mapitems/layer_speedup.h @@ -3,10 +3,24 @@ #include "layer_tiles.h" +struct SSpeedupTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Force; + int m_Angle; + int m_MaxSpeed; + int m_Type; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerSpeedup : public CLayerTiles { public: CLayerSpeedup(CEditor *pEditor, int w, int h); + CLayerSpeedup(const CLayerSpeedup &Other); ~CLayerSpeedup(); CSpeedupTile *m_pSpeedupTile; @@ -22,6 +36,19 @@ public: void BrushFlipY() override; void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; + + EditorTileStateChangeHistory m_History; + void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, SSpeedupTileStateChange::SData Previous, SSpeedupTileStateChange::SData Current); }; #endif diff --git a/src/game/editor/mapitems/layer_switch.cpp b/src/game/editor/mapitems/layer_switch.cpp index ef46a33f1..6d59903b6 100644 --- a/src/game/editor/mapitems/layer_switch.cpp +++ b/src/game/editor/mapitems/layer_switch.cpp @@ -12,6 +12,16 @@ CLayerSwitch::CLayerSwitch(CEditor *pEditor, int w, int h) : mem_zero(m_pSwitchTile, (size_t)w * h * sizeof(CSwitchTile)); } +CLayerSwitch::CLayerSwitch(const CLayerSwitch &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Switch copy"); + m_Switch = 1; + + m_pSwitchTile = new CSwitchTile[m_Width * m_Height]; + mem_copy(m_pSwitchTile, Other.m_pSwitchTile, (size_t)m_Width * m_Height * sizeof(CSwitchTile)); +} + CLayerSwitch::~CLayerSwitch() { delete[] m_pSwitchTile; @@ -83,54 +93,79 @@ void CLayerSwitch::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + SSwitchTileStateChange::SData Previous{ + m_pSwitchTile[Index].m_Number, + m_pSwitchTile[Index].m_Type, + m_pSwitchTile[Index].m_Flags, + m_pSwitchTile[Index].m_Delay, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidSwitchTile(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) && pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index != TILE_AIR) { if(m_pEditor->m_SwitchNum != pSwitchLayer->m_SwitchNumber || m_pEditor->m_SwitchDelay != pSwitchLayer->m_SwitchDelay) { - m_pSwitchTile[fy * m_Width + fx].m_Number = m_pEditor->m_SwitchNum; - m_pSwitchTile[fy * m_Width + fx].m_Delay = m_pEditor->m_SwitchDelay; + m_pSwitchTile[Index].m_Number = m_pEditor->m_SwitchNum; + m_pSwitchTile[Index].m_Delay = m_pEditor->m_SwitchDelay; } else if(pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number) { - m_pSwitchTile[fy * m_Width + fx].m_Number = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number; - m_pSwitchTile[fy * m_Width + fx].m_Delay = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Delay; + m_pSwitchTile[Index].m_Number = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number; + m_pSwitchTile[Index].m_Delay = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Delay; } else { - m_pSwitchTile[fy * m_Width + fx].m_Number = m_pEditor->m_SwitchNum; - m_pSwitchTile[fy * m_Width + fx].m_Delay = m_pEditor->m_SwitchDelay; + m_pSwitchTile[Index].m_Number = m_pEditor->m_SwitchNum; + m_pSwitchTile[Index].m_Delay = m_pEditor->m_SwitchDelay; } - m_pSwitchTile[fy * m_Width + fx].m_Type = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; - m_pSwitchTile[fy * m_Width + fx].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; - m_pTiles[fy * m_Width + fx].m_Index = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; + m_pSwitchTile[Index].m_Type = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; + m_pSwitchTile[Index].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; + m_pTiles[Index].m_Index = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; if(!IsSwitchTileFlagsUsed(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) { - m_pSwitchTile[fy * m_Width + fx].m_Flags = 0; + m_pSwitchTile[Index].m_Flags = 0; } if(!IsSwitchTileNumberUsed(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) { - m_pSwitchTile[fy * m_Width + fx].m_Number = 0; + m_pSwitchTile[Index].m_Number = 0; } if(!IsSwitchTileDelayUsed(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) { - m_pSwitchTile[fy * m_Width + fx].m_Delay = 0; + m_pSwitchTile[Index].m_Delay = 0; } } else { - m_pSwitchTile[fy * m_Width + fx].m_Number = 0; - m_pSwitchTile[fy * m_Width + fx].m_Type = 0; - m_pSwitchTile[fy * m_Width + fx].m_Flags = 0; - m_pSwitchTile[fy * m_Width + fx].m_Delay = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pSwitchTile[Index].m_Number = 0; + m_pSwitchTile[Index].m_Type = 0; + m_pSwitchTile[Index].m_Flags = 0; + m_pSwitchTile[Index].m_Delay = 0; + m_pTiles[Index].m_Index = 0; } + + SSwitchTileStateChange::SData Current{ + m_pSwitchTile[Index].m_Number, + m_pSwitchTile[Index].m_Type, + m_pSwitchTile[Index].m_Flags, + m_pSwitchTile[Index].m_Delay, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pSwitchLayer->m_Width, pSwitchLayer->m_Height); } +void CLayerSwitch::RecordStateChange(int x, int y, SSwitchTileStateChange::SData Previous, SSwitchTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = SSwitchTileStateChange{true, Previous, Current}; + else + m_History[y][x].m_Current = Current; +} + void CLayerSwitch::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -217,6 +252,13 @@ void CLayerSwitch::FillSelection(bool Empty, std::shared_ptr pBrush, CUI const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + SSwitchTileStateChange::SData Previous{ + m_pSwitchTile[TgtIndex].m_Number, + m_pSwitchTile[TgtIndex].m_Type, + m_pSwitchTile[TgtIndex].m_Flags, + m_pSwitchTile[TgtIndex].m_Delay, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidSwitchTile((pLt->m_pTiles[SrcIndex]).m_Index))) { m_pTiles[TgtIndex].m_Index = 0; @@ -250,6 +292,15 @@ void CLayerSwitch::FillSelection(bool Empty, std::shared_ptr pBrush, CUI m_pSwitchTile[TgtIndex].m_Flags = pLt->m_pSwitchTile[SrcIndex].m_Flags; } } + + SSwitchTileStateChange::SData Current{ + m_pSwitchTile[TgtIndex].m_Number, + m_pSwitchTile[TgtIndex].m_Type, + m_pSwitchTile[TgtIndex].m_Flags, + m_pSwitchTile[TgtIndex].m_Delay, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); @@ -270,3 +321,13 @@ bool CLayerSwitch::ContainsElementWithId(int Id) return false; } + +std::shared_ptr CLayerSwitch::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerSwitch::TypeName() const +{ + return "switch"; +} diff --git a/src/game/editor/mapitems/layer_switch.h b/src/game/editor/mapitems/layer_switch.h index 4011105cc..7a8aae79b 100644 --- a/src/game/editor/mapitems/layer_switch.h +++ b/src/game/editor/mapitems/layer_switch.h @@ -3,10 +3,24 @@ #include "layer_tiles.h" +struct SSwitchTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Number; + int m_Type; + int m_Flags; + int m_Delay; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerSwitch : public CLayerTiles { public: CLayerSwitch(CEditor *pEditor, int w, int h); + CLayerSwitch(const CLayerSwitch &Other); ~CLayerSwitch(); CSwitchTile *m_pSwitchTile; @@ -22,6 +36,19 @@ public: void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; virtual bool ContainsElementWithId(int Id); + + EditorTileStateChangeHistory m_History; + inline void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, SSwitchTileStateChange::SData Previous, SSwitchTileStateChange::SData Current); }; #endif diff --git a/src/game/editor/mapitems/layer_tele.cpp b/src/game/editor/mapitems/layer_tele.cpp index 6b397b4db..0006a74c9 100644 --- a/src/game/editor/mapitems/layer_tele.cpp +++ b/src/game/editor/mapitems/layer_tele.cpp @@ -12,6 +12,16 @@ CLayerTele::CLayerTele(CEditor *pEditor, int w, int h) : mem_zero(m_pTeleTile, (size_t)w * h * sizeof(CTeleTile)); } +CLayerTele::CLayerTele(const CLayerTele &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Tele copy"); + m_Tele = 1; + + m_pTeleTile = new CTeleTile[m_Width * m_Height]; + mem_copy(m_pTeleTile, Other.m_pTeleTile, (size_t)m_Width * m_Height * sizeof(CTeleTile)); +} + CLayerTele::~CLayerTele() { delete[] m_pTeleTile; @@ -80,48 +90,80 @@ void CLayerTele::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + STeleTileStateChange::SData Previous{ + m_pTeleTile[Index].m_Number, + m_pTeleTile[Index].m_Type, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidTeleTile(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index)) && pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index != TILE_AIR) { if(!IsTeleTileNumberUsed(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index)) { // Tele tile number is unused. Set a known value which is not 0, // as tiles with number 0 would be ignored by previous versions. - m_pTeleTile[fy * m_Width + fx].m_Number = 255; + m_pTeleTile[Index].m_Number = 255; } else if(m_pEditor->m_TeleNumber != pTeleLayer->m_TeleNum) { - m_pTeleTile[fy * m_Width + fx].m_Number = m_pEditor->m_TeleNumber; + m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumber; } else if(pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number) { - m_pTeleTile[fy * m_Width + fx].m_Number = pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number; + m_pTeleTile[Index].m_Number = pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number; } else { if(!m_pEditor->m_TeleNumber) { - m_pTeleTile[fy * m_Width + fx].m_Number = 0; - m_pTeleTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTeleTile[Index].m_Number = 0; + m_pTeleTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; + + STeleTileStateChange::SData Current{ + m_pTeleTile[Index].m_Number, + m_pTeleTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); continue; } else - m_pTeleTile[fy * m_Width + fx].m_Number = m_pEditor->m_TeleNumber; + { + m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumber; + } } - m_pTeleTile[fy * m_Width + fx].m_Type = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; + m_pTeleTile[Index].m_Type = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; } else { - m_pTeleTile[fy * m_Width + fx].m_Number = 0; - m_pTeleTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTeleTile[Index].m_Number = 0; + m_pTeleTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } + + STeleTileStateChange::SData Current{ + m_pTeleTile[Index].m_Number, + m_pTeleTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pTeleLayer->m_Width, pTeleLayer->m_Height); } +void CLayerTele::RecordStateChange(int x, int y, STeleTileStateChange::SData Previous, STeleTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = STeleTileStateChange{true, Previous, Current}; + else + { + m_History[y][x].m_Current = Current; + } +} + void CLayerTele::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -202,6 +244,11 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + STeleTileStateChange::SData Previous{ + m_pTeleTile[TgtIndex].m_Number, + m_pTeleTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidTeleTile((pLt->m_pTiles[SrcIndex]).m_Index))) { m_pTiles[TgtIndex].m_Index = 0; @@ -227,6 +274,13 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe m_pTeleTile[TgtIndex].m_Number = pLt->m_pTeleTile[SrcIndex].m_Number; } } + + STeleTileStateChange::SData Current{ + m_pTeleTile[TgtIndex].m_Number, + m_pTeleTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); @@ -247,3 +301,13 @@ bool CLayerTele::ContainsElementWithId(int Id) return false; } + +std::shared_ptr CLayerTele::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerTele::TypeName() const +{ + return "tele"; +} diff --git a/src/game/editor/mapitems/layer_tele.h b/src/game/editor/mapitems/layer_tele.h index d33755185..569dd305e 100644 --- a/src/game/editor/mapitems/layer_tele.h +++ b/src/game/editor/mapitems/layer_tele.h @@ -3,10 +3,22 @@ #include "layer_tiles.h" +struct STeleTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Number; + int m_Type; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerTele : public CLayerTiles { public: CLayerTele(CEditor *pEditor, int w, int h); + CLayerTele(const CLayerTele &Other); ~CLayerTele(); CTeleTile *m_pTeleTile; @@ -21,6 +33,21 @@ public: void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; virtual bool ContainsElementWithId(int Id); + + EditorTileStateChangeHistory m_History; + inline void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, STeleTileStateChange::SData Previous, STeleTileStateChange::SData Current); + + friend class CLayerTiles; }; #endif diff --git a/src/game/editor/mapitems/layer_tiles.cpp b/src/game/editor/mapitems/layer_tiles.cpp index f2e79d5e8..c3f953a7a 100644 --- a/src/game/editor/mapitems/layer_tiles.cpp +++ b/src/game/editor/mapitems/layer_tiles.cpp @@ -2,10 +2,13 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include "layer_tiles.h" -#include - #include #include +#include +#include + +#include +#include #include "image.h" @@ -75,10 +78,25 @@ CTile CLayerTiles::GetTile(int x, int y) } void CLayerTiles::SetTile(int x, int y, CTile Tile) +{ + auto CurrentTile = m_pTiles[y * m_Width + x]; + SetTileIgnoreHistory(x, y, Tile); + RecordStateChange(x, y, CurrentTile, Tile); +} + +void CLayerTiles::SetTileIgnoreHistory(int x, int y, CTile Tile) { m_pTiles[y * m_Width + x] = Tile; } +void CLayerTiles::RecordStateChange(int x, int y, CTile Previous, CTile Tile) +{ + if(!m_TilesHistory[y][x].m_Changed) + m_TilesHistory[y][x] = STileStateChange{true, Previous, Tile}; + else + m_TilesHistory[y][x].m_Current = Tile; +} + void CLayerTiles::PrepareForSave() { for(int y = 0; y < m_Height; y++) @@ -579,6 +597,11 @@ std::shared_ptr CLayerTiles::Duplicate() const return std::make_shared(*this); } +const char *CLayerTiles::TypeName() const +{ + return "tiles"; +} + void CLayerTiles::Resize(int NewW, int NewH) { CTile *pNewData = new CTile[NewW * NewH]; @@ -678,7 +701,6 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) static int s_GameTilesButton = 0; if(m_pEditor->DoButton_Editor(&s_GameTilesButton, "Game tiles", 0, &Button, 0, "Constructs game tiles from this layer")) m_pEditor->PopupSelectGametileOpInvoke(m_pEditor->UI()->MouseX(), m_pEditor->UI()->MouseY()); - int Result = m_pEditor->PopupSelectGameTileOpResult(); switch(Result) { @@ -717,17 +739,49 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) const int OffsetX = -pGroup->m_OffsetX / 32; const int OffsetY = -pGroup->m_OffsetY / 32; + static const char *s_apGametileOpNames[] = { + "Air", + "Hookable", + "Death", + "Unhookable", + "Hookthrough", + "Freeze", + "Unfreeze", + "Deep Freeze", + "Deep Unfreeze", + "Blue Check-Tele", + "Red Check-Tele", + "Live Freeze", + "Live Unfreeze", + }; + + std::vector> vpActions; + std::shared_ptr pGLayer = m_pEditor->m_Map.m_pGameLayer; + int GameLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pGLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(); + if(Result != TILE_TELECHECKIN && Result != TILE_TELECHECKINEVIL) { - std::shared_ptr pGLayer = m_pEditor->m_Map.m_pGameLayer; - if(pGLayer->m_Width < m_Width + OffsetX || pGLayer->m_Height < m_Height + OffsetY) { + std::map> savedLayers; + savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate(); + savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES]; + + int PrevW = pGLayer->m_Width; + int PrevH = pGLayer->m_Height; const int NewW = pGLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pGLayer->m_Width; const int NewH = pGLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pGLayer->m_Height; pGLayer->Resize(NewW, NewH); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); + const std::shared_ptr &Action1 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); + const std::shared_ptr &Action2 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); + + Action1->SetSavedLayers(savedLayers); + Action2->SetSavedLayers(savedLayers); } + int Changes = 0; for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++) { for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++) @@ -736,40 +790,111 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { const CTile ResultTile = {(unsigned char)Result}; pGLayer->SetTile(x + OffsetX, y + OffsetY, ResultTile); + Changes++; } } } + + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup)); + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Construct '%s' game tiles (x%d)", s_apGametileOpNames[Result], Changes); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); } else { if(!m_pEditor->m_Map.m_pTeleLayer) { - std::shared_ptr pLayer = std::make_shared(m_pEditor, m_Width, m_Height); + std::shared_ptr pLayer = std::make_shared(m_pEditor, m_Width, m_Height); m_pEditor->m_Map.MakeTeleLayer(pLayer); m_pEditor->m_Map.m_pGameGroup->AddLayer(pLayer); + + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_Map.m_pGameGroup->m_vpLayers.size() - 1)); + + if(m_Width != pGLayer->m_Width || m_Height > pGLayer->m_Height) + { + std::map> savedLayers; + savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate(); + savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES]; + + int NewW = pGLayer->m_Width; + int NewH = pGLayer->m_Height; + if(m_Width > pGLayer->m_Width) + { + NewW = m_Width; + } + if(m_Height > pGLayer->m_Height) + { + NewH = m_Height; + } + + int PrevW = pGLayer->m_Width; + int PrevH = pGLayer->m_Height; + pLayer->Resize(NewW, NewH); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); + const std::shared_ptr &Action1 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); + const std::shared_ptr &Action2 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); + + Action1->SetSavedLayers(savedLayers); + Action2->SetSavedLayers(savedLayers); + } } std::shared_ptr pTLayer = m_pEditor->m_Map.m_pTeleLayer; + int TeleLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pTLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(); if(pTLayer->m_Width < m_Width + OffsetX || pTLayer->m_Height < m_Height + OffsetY) { + std::map> savedLayers; + savedLayers[LAYERTYPE_TILES] = pTLayer->Duplicate(); + savedLayers[LAYERTYPE_TELE] = savedLayers[LAYERTYPE_TILES]; + + int PrevW = pTLayer->m_Width; + int PrevH = pTLayer->m_Height; int NewW = pTLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pTLayer->m_Width; int NewH = pTLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pTLayer->m_Height; pTLayer->Resize(NewW, NewH); + std::shared_ptr Action1, Action2; + vpActions.push_back(Action1 = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); + vpActions.push_back(Action2 = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); + + Action1->SetSavedLayers(savedLayers); + Action2->SetSavedLayers(savedLayers); } + int Changes = 0; for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++) { for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++) { if(GetTile(x, y).m_Index) { - pTLayer->m_pTiles[(y + OffsetY) * pTLayer->m_Width + x + OffsetX].m_Index = TILE_AIR + Result; - pTLayer->m_pTeleTile[(y + OffsetY) * pTLayer->m_Width + x + OffsetX].m_Number = 1; - pTLayer->m_pTeleTile[(y + OffsetY) * pTLayer->m_Width + x + OffsetX].m_Type = TILE_AIR + Result; + auto TileIndex = (y + OffsetY) * pTLayer->m_Width + x + OffsetX; + Changes++; + + STeleTileStateChange::SData Previous{ + pTLayer->m_pTeleTile[TileIndex].m_Number, + pTLayer->m_pTeleTile[TileIndex].m_Type, + pTLayer->m_pTiles[TileIndex].m_Index}; + + pTLayer->m_pTiles[TileIndex].m_Index = TILE_AIR + Result; + pTLayer->m_pTeleTile[TileIndex].m_Number = 1; + pTLayer->m_pTeleTile[TileIndex].m_Type = TILE_AIR + Result; + + STeleTileStateChange::SData Current{ + pTLayer->m_pTeleTile[TileIndex].m_Number, + pTLayer->m_pTeleTile[TileIndex].m_Type, + pTLayer->m_pTiles[TileIndex].m_Index}; + + pTLayer->RecordStateChange(x, y, Previous, Current); } } } + + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup)); + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Construct 'tele' game tiles (x%d)", Changes); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); } } } @@ -790,6 +915,12 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { m_AutoAutoMap = !m_AutoAutoMap; FlagModified(0, 0, m_Width, m_Height); + if(!m_TilesHistory.empty()) // Sometimes pressing that button causes the automap to run so we should be able to undo that + { + // record undo + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_TilesHistory)); + ClearHistory(); + } } } @@ -797,31 +928,15 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) if(m_pEditor->DoButton_Editor(&s_AutoMapperButton, "Automap", 0, &Button, 0, "Run the automapper")) { m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.Proceed(this, m_AutoMapperConfig, m_Seed); + // record undo + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_TilesHistory)); + ClearHistory(); return CUI::POPUP_CLOSE_CURRENT; } } } - enum - { - PROP_WIDTH = 0, - PROP_HEIGHT, - PROP_SHIFT, - PROP_SHIFT_BY, - PROP_IMAGE, - PROP_COLOR, - PROP_COLOR_ENV, - PROP_COLOR_ENV_OFFSET, - PROP_AUTOMAPPER, - PROP_SEED, - NUM_PROPS, - }; - - int Color = 0; - Color |= m_Color.r << 24; - Color |= m_Color.g << 16; - Color |= m_Color.b << 8; - Color |= m_Color.a; + int Color = PackColor(m_Color); CProperty aProps[] = { {"Width", m_Width, PROPTYPE_INT_SCROLL, 1, 100000}, @@ -839,21 +954,24 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) if(EntitiesLayer) // remove the image and color properties if this is a game layer { - aProps[PROP_IMAGE].m_pName = nullptr; - aProps[PROP_COLOR].m_pName = nullptr; - aProps[PROP_AUTOMAPPER].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_IMAGE].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_COLOR].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr; } if(m_Image == -1) { - aProps[PROP_AUTOMAPPER].m_pName = nullptr; - aProps[PROP_SEED].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_SEED].m_pName = nullptr; } - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ETilesProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); + auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); - if(Prop == PROP_WIDTH && NewVal > 1) + static CLayerTilesPropTracker s_Tracker(m_pEditor); + s_Tracker.Begin(this, Prop, State); + + if(Prop == ETilesProp::PROP_WIDTH && NewVal > 1) { if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned) { @@ -863,7 +981,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } Resize(NewVal, m_Height); } - else if(Prop == PROP_HEIGHT && NewVal > 1) + else if(Prop == ETilesProp::PROP_HEIGHT && NewVal > 1) { if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned) { @@ -873,15 +991,15 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } Resize(m_Width, NewVal); } - else if(Prop == PROP_SHIFT) + else if(Prop == ETilesProp::PROP_SHIFT) { Shift(NewVal); } - else if(Prop == PROP_SHIFT_BY) + else if(Prop == ETilesProp::PROP_SHIFT_BY) { m_pEditor->m_ShiftBy = NewVal; } - else if(Prop == PROP_IMAGE) + else if(Prop == ETilesProp::PROP_IMAGE) { m_Image = NewVal; if(NewVal == -1) @@ -901,14 +1019,14 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } } } - else if(Prop == PROP_COLOR) + else if(Prop == ETilesProp::PROP_COLOR) { m_Color.r = (NewVal >> 24) & 0xff; m_Color.g = (NewVal >> 16) & 0xff; m_Color.b = (NewVal >> 8) & 0xff; m_Color.a = NewVal & 0xff; } - else if(Prop == PROP_COLOR_ENV) + else if(Prop == ETilesProp::PROP_COLOR_ENV) { int Index = clamp(NewVal - 1, -1, (int)m_pEditor->m_Map.m_vpEnvelopes.size() - 1); const int Step = (Index - m_ColorEnv) % 2; @@ -924,15 +1042,15 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } } } - else if(Prop == PROP_COLOR_ENV_OFFSET) + else if(Prop == ETilesProp::PROP_COLOR_ENV_OFFSET) { m_ColorEnvOffset = NewVal; } - else if(Prop == PROP_SEED) + else if(Prop == ETilesProp::PROP_SEED) { m_Seed = NewVal; } - else if(Prop == PROP_AUTOMAPPER) + else if(Prop == ETilesProp::PROP_AUTOMAPPER) { if(m_Image >= 0 && m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum() > 0 && NewVal >= 0) m_AutoMapperConfig = NewVal % m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum(); @@ -940,15 +1058,17 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) m_AutoMapperConfig = -1; } - if(Prop != -1) + if(Prop != ETilesProp::PROP_NONE) { FlagModified(0, 0, m_Width, m_Height); } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers) +CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices) { if(State.m_Modified) { @@ -957,22 +1077,77 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta static int s_CommitButton = 0; if(pEditor->DoButton_Editor(&s_CommitButton, "Commit", 0, &Commit, 0, "Applies the changes")) { + bool HasModifiedSize = (State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0; + bool HasModifiedColor = (State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0; + + std::vector> vpActions; + int j = 0; + int GroupIndex = pEditor->m_SelectedGroup; for(auto &pLayer : vpLayers) { - if((State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0) + int LayerIndex = vLayerIndices[j++]; + if(HasModifiedSize) + { + std::map> SavedLayers; + SavedLayers[LAYERTYPE_TILES] = pLayer->Duplicate(); + if(pLayer->m_Game || pLayer->m_Front || pLayer->m_Switch || pLayer->m_Speedup || pLayer->m_Tune || pLayer->m_Tele) + { // Need to save all entities layers when any entity layer + if(pEditor->m_Map.m_pFrontLayer && !pLayer->m_Front) + SavedLayers[LAYERTYPE_FRONT] = pEditor->m_Map.m_pFrontLayer->Duplicate(); + if(pEditor->m_Map.m_pTeleLayer && !pLayer->m_Tele) + SavedLayers[LAYERTYPE_TELE] = pEditor->m_Map.m_pTeleLayer->Duplicate(); + if(pEditor->m_Map.m_pSwitchLayer && !pLayer->m_Switch) + SavedLayers[LAYERTYPE_SWITCH] = pEditor->m_Map.m_pSwitchLayer->Duplicate(); + if(pEditor->m_Map.m_pSpeedupLayer && !pLayer->m_Speedup) + SavedLayers[LAYERTYPE_SPEEDUP] = pEditor->m_Map.m_pSpeedupLayer->Duplicate(); + if(pEditor->m_Map.m_pTuneLayer && !pLayer->m_Tune) + SavedLayers[LAYERTYPE_TUNE] = pEditor->m_Map.m_pTuneLayer->Duplicate(); + if(!pLayer->m_Game) + SavedLayers[LAYERTYPE_GAME] = pEditor->m_Map.m_pGameLayer->Duplicate(); + } + + int PrevW = pLayer->m_Width; + int PrevH = pLayer->m_Height; pLayer->Resize(State.m_Width, State.m_Height); - if((State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0) + if(PrevW != State.m_Width) + { + std::shared_ptr pAction; + vpActions.push_back(pAction = std::make_shared(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_WIDTH, PrevW, State.m_Width)); + pAction->SetSavedLayers(SavedLayers); + } + + if(PrevH != State.m_Height) + { + std::shared_ptr pAction; + vpActions.push_back(pAction = std::make_shared(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_HEIGHT, PrevH, State.m_Height)); + pAction->SetSavedLayers(SavedLayers); + } + } + + if(HasModifiedColor) { + int Color = 0; + Color |= pLayer->m_Color.r << 24; + Color |= pLayer->m_Color.g << 16; + Color |= pLayer->m_Color.b << 8; + Color |= pLayer->m_Color.a; + pLayer->m_Color.r = (State.m_Color >> 24) & 0xff; pLayer->m_Color.g = (State.m_Color >> 16) & 0xff; pLayer->m_Color.b = (State.m_Color >> 8) & 0xff; pLayer->m_Color.a = State.m_Color & 0xff; + + vpActions.push_back(std::make_shared(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_COLOR, Color, State.m_Color)); } pLayer->FlagModified(0, 0, pLayer->m_Width, pLayer->m_Height); } State.m_Modified = 0; + + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Edit %d layers common properties: %s", (int)vpLayers.size(), HasModifiedColor && HasModifiedSize ? "color, size" : (HasModifiedColor ? "color" : "size")); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, vpActions, aDisplay)); } } else @@ -999,16 +1174,6 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta pToolbox->HSplitTop(2.0f, nullptr, pToolbox); } - enum - { - PROP_WIDTH = 0, - PROP_HEIGHT, - PROP_SHIFT, - PROP_SHIFT_BY, - PROP_COLOR, - NUM_PROPS, - }; - CProperty aProps[] = { {"Width", State.m_Width, PROPTYPE_INT_SCROLL, 1, 100000}, {"Height", State.m_Height, PROPTYPE_INT_SCROLL, 1, 100000}, @@ -1018,11 +1183,17 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ETilesCommonProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(pToolbox, aProps, s_aIds, &NewVal); + auto [PropState, Prop] = pEditor->DoPropertiesWithState(pToolbox, aProps, s_aIds, &NewVal); - if(Prop == PROP_WIDTH && NewVal > 1) + static CLayerTilesCommonPropTracker s_Tracker(pEditor); + s_Tracker.m_vpLayers = vpLayers; + s_Tracker.m_vLayerIndices = vLayerIndices; + + s_Tracker.Begin(nullptr, Prop, PropState); + + if(Prop == ETilesCommonProp::PROP_WIDTH && NewVal > 1) { if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned) { @@ -1032,7 +1203,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta } State.m_Width = NewVal; } - else if(Prop == PROP_HEIGHT && NewVal > 1) + else if(Prop == ETilesCommonProp::PROP_HEIGHT && NewVal > 1) { if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned) { @@ -1042,25 +1213,27 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta } State.m_Height = NewVal; } - else if(Prop == PROP_SHIFT) + else if(Prop == ETilesCommonProp::PROP_SHIFT) { for(auto &pLayer : vpLayers) pLayer->Shift(NewVal); } - else if(Prop == PROP_SHIFT_BY) + else if(Prop == ETilesCommonProp::PROP_SHIFT_BY) { pEditor->m_ShiftBy = NewVal; } - else if(Prop == PROP_COLOR) + else if(Prop == ETilesCommonProp::PROP_COLOR) { State.m_Color = NewVal; } - if(Prop == PROP_WIDTH || Prop == PROP_HEIGHT) + s_Tracker.End(Prop, PropState); + + if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT) { State.m_Modified |= SCommonPropState::MODIFIED_SIZE; } - else if(Prop == PROP_COLOR) + else if(Prop == ETilesCommonProp::PROP_COLOR) { State.m_Modified |= SCommonPropState::MODIFIED_COLOR; } diff --git a/src/game/editor/mapitems/layer_tiles.h b/src/game/editor/mapitems/layer_tiles.h index 48717db3c..10e7fc25a 100644 --- a/src/game/editor/mapitems/layer_tiles.h +++ b/src/game/editor/mapitems/layer_tiles.h @@ -1,8 +1,21 @@ #ifndef GAME_EDITOR_MAPITEMS_LAYER_TILES_H #define GAME_EDITOR_MAPITEMS_LAYER_TILES_H +#include +#include + #include "layer.h" +struct STileStateChange +{ + bool m_Changed; + CTile m_Previous; + CTile m_Current; +}; + +template +using EditorTileStateChangeHistory = std::map>; + enum { DIRECTION_LEFT = 0, @@ -89,6 +102,7 @@ public: virtual CTile GetTile(int x, int y); virtual void SetTile(int x, int y, CTile Tile); + void SetTileIgnoreHistory(int x, int y, CTile Tile); virtual void Resize(int NewW, int NewH); virtual void Shift(int Direction); @@ -114,6 +128,7 @@ public: void BrushRotate(float Amount) override; std::shared_ptr Duplicate() const override; + const char *TypeName() const override; virtual void ShowInfo(); CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; @@ -130,7 +145,7 @@ public: int m_Height = -1; int m_Color = 0; }; - static CUI::EPopupMenuFunctionResult RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers); + static CUI::EPopupMenuFunctionResult RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices); void ModifyImageIndex(FIndexModifyFunction pfnFunc) override; void ModifyEnvelopeIndex(FIndexModifyFunction pfnFunc) override; @@ -166,6 +181,14 @@ public: int m_Switch; int m_Tune; char m_aFileName[IO_MAX_PATH_LENGTH]; + + EditorTileStateChangeHistory m_TilesHistory; + inline virtual void ClearHistory() { m_TilesHistory.clear(); } + +protected: + void RecordStateChange(int x, int y, CTile Previous, CTile Tile); + + friend class CAutoMapper; }; #endif diff --git a/src/game/editor/mapitems/layer_tune.cpp b/src/game/editor/mapitems/layer_tune.cpp index 619336943..0a41fcc7b 100644 --- a/src/game/editor/mapitems/layer_tune.cpp +++ b/src/game/editor/mapitems/layer_tune.cpp @@ -12,6 +12,16 @@ CLayerTune::CLayerTune(CEditor *pEditor, int w, int h) : mem_zero(m_pTuneTile, (size_t)w * h * sizeof(CTuneTile)); } +CLayerTune::CLayerTune(const CLayerTune &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Tune copy"); + m_Tune = 1; + + m_pTuneTile = new CTuneTile[m_Width * m_Height]; + mem_copy(m_pTuneTile, Other.m_pTuneTile, (size_t)m_Width * m_Height * sizeof(CTuneTile)); +} + CLayerTune::~CLayerTune() { delete[] m_pTuneTile; @@ -82,40 +92,61 @@ void CLayerTune::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + STuneTileStateChange::SData Previous{ + m_pTuneTile[Index].m_Number, + m_pTuneTile[Index].m_Type, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidTuneTile(pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index)) && pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index != TILE_AIR) { if(m_pEditor->m_TuningNum != pTuneLayer->m_TuningNumber) { - m_pTuneTile[fy * m_Width + fx].m_Number = m_pEditor->m_TuningNum; + m_pTuneTile[Index].m_Number = m_pEditor->m_TuningNum; } else if(pTuneLayer->m_pTuneTile[y * pTuneLayer->m_Width + x].m_Number) - m_pTuneTile[fy * m_Width + fx].m_Number = pTuneLayer->m_pTuneTile[y * pTuneLayer->m_Width + x].m_Number; + m_pTuneTile[Index].m_Number = pTuneLayer->m_pTuneTile[y * pTuneLayer->m_Width + x].m_Number; else { if(!m_pEditor->m_TuningNum) { - m_pTuneTile[fy * m_Width + fx].m_Number = 0; - m_pTuneTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTuneTile[Index].m_Number = 0; + m_pTuneTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; continue; } else - m_pTuneTile[fy * m_Width + fx].m_Number = m_pEditor->m_TuningNum; + m_pTuneTile[Index].m_Number = m_pEditor->m_TuningNum; } - m_pTuneTile[fy * m_Width + fx].m_Type = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; + m_pTuneTile[Index].m_Type = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; } else { - m_pTuneTile[fy * m_Width + fx].m_Number = 0; - m_pTuneTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTuneTile[Index].m_Number = 0; + m_pTuneTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } + + STuneTileStateChange::SData Current{ + m_pTuneTile[Index].m_Number, + m_pTuneTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pTuneLayer->m_Width, pTuneLayer->m_Height); } +void CLayerTune::RecordStateChange(int x, int y, STuneTileStateChange::SData Previous, STuneTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = STuneTileStateChange{true, Previous, Current}; + else + m_History[y][x].m_Current = Current; +} + void CLayerTune::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -194,6 +225,11 @@ void CLayerTune::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + STuneTileStateChange::SData Previous{ + m_pTuneTile[TgtIndex].m_Number, + m_pTuneTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidTuneTile((pLt->m_pTiles[SrcIndex]).m_Index))) { m_pTiles[TgtIndex].m_Index = 0; @@ -213,8 +249,25 @@ void CLayerTune::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe m_pTuneTile[TgtIndex].m_Number = pLt->m_pTuneTile[SrcIndex].m_Number; } } + + STuneTileStateChange::SData Current{ + m_pTuneTile[TgtIndex].m_Number, + m_pTuneTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); } + +std::shared_ptr CLayerTune::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerTune::TypeName() const +{ + return "tune"; +} diff --git a/src/game/editor/mapitems/layer_tune.h b/src/game/editor/mapitems/layer_tune.h index 8706778ec..cf35b9569 100644 --- a/src/game/editor/mapitems/layer_tune.h +++ b/src/game/editor/mapitems/layer_tune.h @@ -3,10 +3,22 @@ #include "layer_tiles.h" +struct STuneTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Number; + int m_Type; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerTune : public CLayerTiles { public: CLayerTune(CEditor *pEditor, int w, int h); + CLayerTune(const CLayerTune &Other); ~CLayerTune(); CTuneTile *m_pTuneTile; @@ -20,6 +32,19 @@ public: void BrushFlipY() override; void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; + + EditorTileStateChangeHistory m_History; + inline void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, STuneTileStateChange::SData Previous, STuneTileStateChange::SData Current); }; #endif diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 0dd22b9b0..6150965fe 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -16,6 +16,7 @@ #include #include "editor.h" +#include "editor_actions.h" CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect View, bool Active) { @@ -344,6 +345,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { if(pEditor->DoButton_Editor(&s_DeleteButton, "Delete group", 0, &Button, 0, "Delete group")) { + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, true)); pEditor->m_Map.DeleteGroup(pEditor->m_SelectedGroup); pEditor->m_SelectedGroup = maximum(0, pEditor->m_SelectedGroup - 1); return CUI::POPUP_CLOSE_CURRENT; @@ -403,8 +405,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, std::shared_ptr pTeleLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); pEditor->m_Map.MakeTeleLayer(pTeleLayer); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTeleLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_pBrush->Clear(); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } } @@ -420,8 +424,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, std::shared_ptr pSpeedupLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); pEditor->m_Map.MakeSpeedupLayer(pSpeedupLayer); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSpeedupLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_pBrush->Clear(); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } } @@ -437,8 +443,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, std::shared_ptr pTuneLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); pEditor->m_Map.MakeTuneLayer(pTuneLayer); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTuneLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_pBrush->Clear(); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } } @@ -454,8 +462,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, std::shared_ptr pFrontLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); pEditor->m_Map.MakeFrontLayer(pFrontLayer); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pFrontLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_pBrush->Clear(); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } } @@ -471,8 +481,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, std::shared_ptr pSwitchLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); pEditor->m_Map.MakeSwitchLayer(pSwitchLayer); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSwitchLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_pBrush->Clear(); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } } @@ -485,8 +497,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { std::shared_ptr pQuadLayer = std::make_shared(pEditor); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pQuadLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } @@ -499,8 +513,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, std::shared_ptr pTileLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); pTileLayer->m_pEditor = pEditor; pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTileLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } @@ -512,8 +528,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { std::shared_ptr pSoundLayer = std::make_shared(pEditor); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSoundLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; + pEditor->SelectLayer(LayerIndex); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); return CUI::POPUP_CLOSE_CURRENT; } @@ -530,21 +548,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, pEditor->m_Map.OnModify(); } - enum - { - PROP_ORDER = 0, - PROP_POS_X, - PROP_POS_Y, - PROP_PARA_X, - PROP_PARA_Y, - PROP_USE_CLIPPING, - PROP_CLIP_X, - PROP_CLIP_Y, - PROP_CLIP_W, - PROP_CLIP_H, - NUM_PROPS, - }; - CProperty aProps[] = { {"Order", pEditor->m_SelectedGroup, PROPTYPE_INT_STEP, 0, (int)pEditor->m_Map.m_vpGroups.size() - 1}, {"Pos X", -pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX, PROPTYPE_INT_SCROLL, -1000000, 1000000}, @@ -561,17 +564,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // cut the properties that aren't needed if(pEditor->GetSelectedGroup()->m_GameGroup) - aProps[PROP_POS_X].m_pName = nullptr; + aProps[(int)EGroupProp::PROP_POS_X].m_pName = nullptr; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)EGroupProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != EGroupProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_ORDER) + static CLayerGroupPropTracker s_Tracker(pEditor); + s_Tracker.Begin(pEditor->GetSelectedGroup().get(), Prop, State); + + if(Prop == EGroupProp::PROP_ORDER) { pEditor->m_SelectedGroup = pEditor->m_Map.SwapGroups(pEditor->m_SelectedGroup, NewVal); } @@ -579,44 +585,46 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // these can not be changed on the game group if(!pEditor->GetSelectedGroup()->m_GameGroup) { - if(Prop == PROP_PARA_X) + if(Prop == EGroupProp::PROP_PARA_X) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxX = NewVal; } - else if(Prop == PROP_PARA_Y) + else if(Prop == EGroupProp::PROP_PARA_Y) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxY = NewVal; } - else if(Prop == PROP_POS_X) + else if(Prop == EGroupProp::PROP_POS_X) { - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX = -NewVal; + pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX = NewVal; } - else if(Prop == PROP_POS_Y) + else if(Prop == EGroupProp::PROP_POS_Y) { - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetY = -NewVal; + pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetY = NewVal; } - else if(Prop == PROP_USE_CLIPPING) + else if(Prop == EGroupProp::PROP_USE_CLIPPING) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_UseClipping = NewVal; } - else if(Prop == PROP_CLIP_X) + else if(Prop == EGroupProp::PROP_CLIP_X) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipX = NewVal; } - else if(Prop == PROP_CLIP_Y) + else if(Prop == EGroupProp::PROP_CLIP_Y) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipY = NewVal; } - else if(Prop == PROP_CLIP_W) + else if(Prop == EGroupProp::PROP_CLIP_W) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipW = NewVal; } - else if(Prop == PROP_CLIP_H) + else if(Prop == EGroupProp::PROP_CLIP_H) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipH = NewVal; } } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } @@ -630,7 +638,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(pPopup->m_vpLayers.size() > 1) { - return CLayerTiles::RenderCommonProperties(pPopup->m_CommonPropState, pEditor, &View, pPopup->m_vpLayers); + return CLayerTiles::RenderCommonProperties(pPopup->m_CommonPropState, pEditor, &View, pPopup->m_vpLayers, pPopup->m_vLayerIndices); } const bool EntitiesLayer = pCurrentLayer->IsEntitiesLayer(); @@ -643,6 +651,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, static int s_DeleteButton = 0; if(pEditor->DoButton_Editor(&s_DeleteButton, "Delete layer", 0, &DeleteButton, 0, "Deletes the layer")) { + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0])); + if(pCurrentLayer == pEditor->m_Map.m_pFrontLayer) pEditor->m_Map.m_pFrontLayer = nullptr; if(pCurrentLayer == pEditor->m_Map.m_pTeleLayer) @@ -654,6 +664,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(pCurrentLayer == pEditor->m_Map.m_pTuneLayer) pEditor->m_Map.m_pTuneLayer = nullptr; pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->DeleteLayer(pEditor->m_vSelectedLayers[0]); + return CUI::POPUP_CLOSE_CURRENT; } } @@ -668,6 +679,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(pEditor->DoButton_Editor(&s_DuplicationButton, "Duplicate layer", 0, &DuplicateButton, 0, "Duplicates the layer")) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->DuplicateLayer(pEditor->m_vSelectedLayers[0]); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0] + 1, true)); return CUI::POPUP_CLOSE_CURRENT; } } @@ -690,14 +702,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(!EntitiesLayer || pEditor->m_Map.m_pGameLayer != pCurrentLayer) View.HSplitBottom(10.0f, &View, nullptr); - enum - { - PROP_GROUP = 0, - PROP_ORDER, - PROP_HQ, - NUM_PROPS, - }; - CProperty aProps[] = { {"Group", pEditor->m_SelectedGroup, PROPTYPE_INT_STEP, 0, (int)pEditor->m_Map.m_vpGroups.size() - 1}, {"Order", pEditor->m_vSelectedLayers[0], PROPTYPE_INT_STEP, 0, (int)pCurrentGroup->m_vpLayers.size() - 1}, @@ -718,19 +722,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, aProps[2].m_Type = PROPTYPE_NULL; } - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ELayerProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != ELayerProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_ORDER) + static CLayerPropTracker s_Tracker(pEditor); + s_Tracker.Begin(pCurrentLayer.get(), Prop, State); + + if(Prop == ELayerProp::PROP_ORDER) { - pEditor->SelectLayer(pCurrentGroup->SwapLayers(pEditor->m_vSelectedLayers[0], NewVal)); + int NewIndex = pCurrentGroup->SwapLayers(pEditor->m_vSelectedLayers[0], NewVal); + pEditor->SelectLayer(NewIndex); } - else if(Prop == PROP_GROUP) + else if(Prop == ELayerProp::PROP_GROUP) { if(NewVal >= 0 && (size_t)NewVal < pEditor->m_Map.m_vpGroups.size()) { @@ -742,13 +750,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[NewVal]->m_vpLayers.size() - 1); } } - else if(Prop == PROP_HQ) + else if(Prop == ELayerProp::PROP_HQ) { pCurrentLayer->m_Flags &= ~LAYERFLAG_DETAIL; if(NewVal) pCurrentLayer->m_Flags |= LAYERFLAG_DETAIL; } + s_Tracker.End(Prop, State); + return pCurrentLayer->RenderProperties(&View); } @@ -782,6 +792,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b static int s_AspectRatioButton = 0; if(pEditor->DoButton_Editor(&s_AspectRatioButton, "Aspect ratio", 0, &Button, 0, "Resizes the current Quad based on the aspect ratio of the image")) { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); for(auto &pQuad : vpQuads) { int Top = pQuad->m_aPoints[0].y; @@ -810,6 +821,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[3].y = Top + Height; pEditor->m_Map.OnModify(); } + pEditor->m_QuadTracker.EndQuadTrack(); + return CUI::POPUP_CLOSE_CURRENT; } } @@ -820,6 +833,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b static int s_AlignButton = 0; if(pEditor->DoButton_Editor(&s_AlignButton, "Align", 0, &Button, 0, "Aligns coordinates of the quad points")) { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); for(auto &pQuad : vpQuads) { for(int k = 1; k < 4; k++) @@ -829,6 +843,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } pEditor->m_Map.OnModify(); } + pEditor->m_QuadTracker.EndQuadTrack(); return CUI::POPUP_CLOSE_CURRENT; } @@ -838,6 +853,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b static int s_Button = 0; if(pEditor->DoButton_Editor(&s_Button, "Square", 0, &Button, 0, "Squares the current quad")) { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); for(auto &pQuad : vpQuads) { int Top = pQuad->m_aPoints[0].y; @@ -867,6 +883,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[3].y = Bottom; pEditor->m_Map.OnModify(); } + pEditor->m_QuadTracker.EndQuadTrack(); return CUI::POPUP_CLOSE_CURRENT; } @@ -881,18 +898,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b return CUI::POPUP_CLOSE_CURRENT; } - enum - { - PROP_ORDER = 0, - PROP_POS_X, - PROP_POS_Y, - PROP_POS_ENV, - PROP_POS_ENV_OFFSET, - PROP_COLOR_ENV, - PROP_COLOR_ENV_OFFSET, - NUM_PROPS, - }; - const int NumQuads = pLayer ? (int)pLayer->m_vQuads.size() : 0; CProperty aProps[] = { {"Order", pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex], PROPTYPE_INT_STEP, 0, NumQuads}, @@ -905,18 +910,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)EQuadProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto PropRes = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + EQuadProp Prop = PropRes.m_Value; + if(Prop != EQuadProp::PROP_NONE) { pEditor->m_Map.OnModify(); + if(PropRes.m_State == EEditState::START || PropRes.m_State == EEditState::ONE_GO) + { + pEditor->m_QuadTracker.BeginQuadPropTrack(pLayer, pEditor->m_vSelectedQuads, Prop); + } } const float OffsetX = i2fx(NewVal) - pCurrentQuad->m_aPoints[4].x; const float OffsetY = i2fx(NewVal) - pCurrentQuad->m_aPoints[4].y; - if(Prop == PROP_ORDER && pLayer) + if(Prop == EQuadProp::PROP_ORDER && pLayer) { const int QuadIndex = pLayer->SwapQuads(pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex], NewVal); pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex] = QuadIndex; @@ -924,17 +934,17 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b for(auto &pQuad : vpQuads) { - if(Prop == PROP_POS_X) + if(Prop == EQuadProp::PROP_POS_X) { for(auto &Point : pQuad->m_aPoints) Point.x += OffsetX; } - else if(Prop == PROP_POS_Y) + else if(Prop == EQuadProp::PROP_POS_Y) { for(auto &Point : pQuad->m_aPoints) Point.y += OffsetY; } - else if(Prop == PROP_POS_ENV) + else if(Prop == EQuadProp::PROP_POS_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); int StepDirection = Index < pQuad->m_PosEnv ? -1 : 1; @@ -950,11 +960,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } } } - else if(Prop == PROP_POS_ENV_OFFSET) + else if(Prop == EQuadProp::PROP_POS_ENV_OFFSET) { pQuad->m_PosEnvOffset = NewVal; } - else if(Prop == PROP_COLOR_ENV) + else if(Prop == EQuadProp::PROP_COLOR_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); int StepDirection = Index < pQuad->m_ColorEnv ? -1 : 1; @@ -970,12 +980,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } } } - else if(Prop == PROP_COLOR_ENV_OFFSET) + else if(Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) { pQuad->m_ColorEnvOffset = NewVal; } } + if(Prop != EQuadProp::PROP_NONE) + { + if(PropRes.m_State == EEditState::END || PropRes.m_State == EEditState::ONE_GO) + { + pEditor->m_QuadTracker.EndQuadPropTrack(Prop); + } + } + return CUI::POPUP_KEEP_OPEN; } @@ -983,6 +1001,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, { CEditor *pEditor = static_cast(pContext); CSoundSource *pSource = pEditor->GetSelectedSource(); + if(!pSource) + return CUI::POPUP_CLOSE_CURRENT; CUIRect Button; @@ -994,9 +1014,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayerType(0, LAYERTYPE_SOUNDS)); if(pLayer) { - pEditor->m_Map.OnModify(); - pLayer->m_vSources.erase(pLayer->m_vSources.begin() + pEditor->m_SelectedSource); - pEditor->m_SelectedSource--; + pEditor->m_EditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0], pEditor->m_SelectedSource)); } return CUI::POPUP_CLOSE_CURRENT; } @@ -1015,40 +1033,9 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, static int s_ShapeTypeButton = 0; if(pEditor->DoButton_Editor(&s_ShapeTypeButton, s_apShapeNames[pSource->m_Shape.m_Type], 0, &ShapeButton, 0, "Change shape")) { - pSource->m_Shape.m_Type = (pSource->m_Shape.m_Type + 1) % CSoundShape::NUM_SHAPES; - - // set default values - switch(pSource->m_Shape.m_Type) - { - case CSoundShape::SHAPE_CIRCLE: - { - pSource->m_Shape.m_Circle.m_Radius = 1000.0f; - break; - } - case CSoundShape::SHAPE_RECTANGLE: - { - pSource->m_Shape.m_Rectangle.m_Width = f2fx(1000.0f); - pSource->m_Shape.m_Rectangle.m_Height = f2fx(800.0f); - break; - } - } + pEditor->m_EditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0], pEditor->m_SelectedSource, CEditorActionEditSoundSource::EEditType::SHAPE, (pSource->m_Shape.m_Type + 1) % CSoundShape::NUM_SHAPES)); } - enum - { - PROP_POS_X = 0, - PROP_POS_Y, - PROP_LOOP, - PROP_PAN, - PROP_TIME_DELAY, - PROP_FALLOFF, - PROP_POS_ENV, - PROP_POS_ENV_OFFSET, - PROP_SOUND_ENV, - PROP_SOUND_ENV_OFFSET, - NUM_PROPS, - }; - CProperty aProps[] = { {"Pos X", pSource->m_Position.x / 1000, PROPTYPE_INT_SCROLL, -1000000, 1000000}, {"Pos Y", pSource->m_Position.y / 1000, PROPTYPE_INT_SCROLL, -1000000, 1000000}, @@ -1063,39 +1050,42 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ESoundProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != ESoundProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_POS_X) + static CSoundSourcePropTracker s_Tracker(pEditor); + s_Tracker.Begin(pSource, Prop, State); + + if(Prop == ESoundProp::PROP_POS_X) { pSource->m_Position.x = NewVal * 1000; } - else if(Prop == PROP_POS_Y) + else if(Prop == ESoundProp::PROP_POS_Y) { pSource->m_Position.y = NewVal * 1000; } - else if(Prop == PROP_LOOP) + else if(Prop == ESoundProp::PROP_LOOP) { pSource->m_Loop = NewVal; } - else if(Prop == PROP_PAN) + else if(Prop == ESoundProp::PROP_PAN) { pSource->m_Pan = NewVal; } - else if(Prop == PROP_TIME_DELAY) + else if(Prop == ESoundProp::PROP_TIME_DELAY) { pSource->m_TimeDelay = NewVal; } - else if(Prop == PROP_FALLOFF) + else if(Prop == ESoundProp::PROP_FALLOFF) { pSource->m_Falloff = NewVal; } - else if(Prop == PROP_POS_ENV) + else if(Prop == ESoundProp::PROP_POS_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); const int StepDirection = Index < pSource->m_PosEnv ? -1 : 1; @@ -1108,11 +1098,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, } } } - else if(Prop == PROP_POS_ENV_OFFSET) + else if(Prop == ESoundProp::PROP_POS_ENV_OFFSET) { pSource->m_PosEnvOffset = NewVal; } - else if(Prop == PROP_SOUND_ENV) + else if(Prop == ESoundProp::PROP_SOUND_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); const int StepDirection = Index < pSource->m_SoundEnv ? -1 : 1; @@ -1125,75 +1115,72 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, } } } - else if(Prop == PROP_SOUND_ENV_OFFSET) + else if(Prop == ESoundProp::PROP_SOUND_ENV_OFFSET) { pSource->m_SoundEnvOffset = NewVal; } + s_Tracker.End(Prop, State); + // source shape properties switch(pSource->m_Shape.m_Type) { case CSoundShape::SHAPE_CIRCLE: { - enum - { - PROP_CIRCLE_RADIUS = 0, - NUM_CIRCLE_PROPS, - }; - CProperty aCircleProps[] = { {"Radius", pSource->m_Shape.m_Circle.m_Radius, PROPTYPE_INT_SCROLL, 0, 1000000}, {nullptr}, }; - static int s_aCircleIds[NUM_CIRCLE_PROPS] = {0}; + static int s_aCircleIds[(int)ECircleShapeProp::NUM_CIRCLE_PROPS] = {0}; NewVal = 0; - Prop = pEditor->DoProperties(&View, aCircleProps, s_aCircleIds, &NewVal); - if(Prop != -1) + auto [LocalState, LocalProp] = pEditor->DoPropertiesWithState(&View, aCircleProps, s_aCircleIds, &NewVal); + if(LocalProp != ECircleShapeProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_CIRCLE_RADIUS) + static CSoundSourceCircleShapePropTracker s_ShapeTracker(pEditor); + s_ShapeTracker.Begin(pSource, LocalProp, LocalState); + + if(LocalProp == ECircleShapeProp::PROP_CIRCLE_RADIUS) { pSource->m_Shape.m_Circle.m_Radius = NewVal; } + s_ShapeTracker.End(LocalProp, LocalState); break; } case CSoundShape::SHAPE_RECTANGLE: { - enum - { - PROP_RECTANGLE_WIDTH = 0, - PROP_RECTANGLE_HEIGHT, - NUM_RECTANGLE_PROPS, - }; - CProperty aRectangleProps[] = { {"Width", pSource->m_Shape.m_Rectangle.m_Width / 1024, PROPTYPE_INT_SCROLL, 0, 1000000}, {"Height", pSource->m_Shape.m_Rectangle.m_Height / 1024, PROPTYPE_INT_SCROLL, 0, 1000000}, {nullptr}, }; - static int s_aRectangleIds[NUM_RECTANGLE_PROPS] = {0}; + static int s_aRectangleIds[(int)ERectangleShapeProp::NUM_RECTANGLE_PROPS] = {0}; NewVal = 0; - Prop = pEditor->DoProperties(&View, aRectangleProps, s_aRectangleIds, &NewVal); - if(Prop != -1) + auto [LocalState, LocalProp] = pEditor->DoPropertiesWithState(&View, aRectangleProps, s_aRectangleIds, &NewVal); + if(LocalProp != ERectangleShapeProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_RECTANGLE_WIDTH) + static CSoundSourceRectShapePropTracker s_ShapeTracker(pEditor); + s_ShapeTracker.Begin(pSource, LocalProp, LocalState); + + if(LocalProp == ERectangleShapeProp::PROP_RECTANGLE_WIDTH) { pSource->m_Shape.m_Rectangle.m_Width = NewVal * 1024; } - else if(Prop == PROP_RECTANGLE_HEIGHT) + else if(LocalProp == ERectangleShapeProp::PROP_RECTANGLE_HEIGHT) { pSource->m_Shape.m_Rectangle.m_Height = NewVal * 1024; } + s_ShapeTracker.End(LocalProp, LocalState); break; } } @@ -1208,22 +1195,9 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, if(!in_range(pEditor->m_SelectedQuadIndex, 0, vpQuads.size() - 1)) return CUI::POPUP_CLOSE_CURRENT; CQuad *pCurrentQuad = vpQuads[pEditor->m_SelectedQuadIndex]; + std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayerType(0, LAYERTYPE_QUADS)); - enum - { - PROP_POS_X = 0, - PROP_POS_Y, - PROP_COLOR, - PROP_TEX_U, - PROP_TEX_V, - NUM_PROPS, - }; - - int Color = 0; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].r << 24; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].g << 16; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].b << 8; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].a; + int Color = PackColor(pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint]); const int X = fx2i(pCurrentQuad->m_aPoints[pEditor->m_SelectedQuadPoint].x); const int Y = fx2i(pCurrentQuad->m_aPoints[pEditor->m_SelectedQuadPoint].y); @@ -1239,29 +1213,35 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)EQuadPointProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto PropRes = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + EQuadPointProp Prop = PropRes.m_Value; + if(Prop != EQuadPointProp::PROP_NONE) { pEditor->m_Map.OnModify(); + if(PropRes.m_State == EEditState::START || PropRes.m_State == EEditState::ONE_GO) + { + pEditor->m_QuadTracker.BeginQuadPointPropTrack(pLayer, pEditor->m_vSelectedQuads, pEditor->m_SelectedQuadPoints); + pEditor->m_QuadTracker.AddQuadPointPropTrack(Prop); + } } for(CQuad *pQuad : vpQuads) { - if(Prop == PROP_POS_X) + if(Prop == EQuadPointProp::PROP_POS_X) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) pQuad->m_aPoints[v].x = i2fx(fx2i(pQuad->m_aPoints[v].x) + NewVal - X); } - else if(Prop == PROP_POS_Y) + else if(Prop == EQuadPointProp::PROP_POS_Y) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) pQuad->m_aPoints[v].y = i2fx(fx2i(pQuad->m_aPoints[v].y) + NewVal - Y); } - else if(Prop == PROP_COLOR) + else if(Prop == EQuadPointProp::PROP_COLOR) { for(int v = 0; v < 4; v++) { @@ -1274,13 +1254,13 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, } } } - else if(Prop == PROP_TEX_U) + else if(Prop == EQuadPointProp::PROP_TEX_U) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) pQuad->m_aTexcoords[v].x = f2fx(fx2f(pQuad->m_aTexcoords[v].x) + (NewVal - TextureU) / 1024.0f); } - else if(Prop == PROP_TEX_V) + else if(Prop == EQuadPointProp::PROP_TEX_V) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) @@ -1288,12 +1268,24 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, } } + if(Prop != EQuadPointProp::PROP_NONE) + { + pEditor->m_Map.OnModify(); + if(PropRes.m_State == EEditState::END || PropRes.m_State == EEditState::ONE_GO) + { + pEditor->m_QuadTracker.EndQuadPointPropTrack(Prop); + } + } + return CUI::POPUP_KEEP_OPEN; } CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); + if(pEditor->m_SelectedEnvelope < 0 || pEditor->m_SelectedEnvelope >= (int)pEditor->m_Map.m_vpEnvelopes.size()) + return CUI::POPUP_CLOSE_CURRENT; + const float RowHeight = 12.0f; CUIRect Row, Label, EditBox; @@ -1309,14 +1301,41 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie Row.VSplitLeft(10.0f, nullptr, &EditBox); pEditor->UI()->DoLabel(&Label, "Color:", RowHeight - 2.0f, TEXTALIGN_ML); - const auto [SelectedIndex, _] = pEditor->m_vSelectedEnvelopePoints.front(); + const auto SelectedPoint = pEditor->m_vSelectedEnvelopePoints.front(); + const int SelectedIndex = SelectedPoint.first; auto *pValues = pEnvelope->m_vPoints[SelectedIndex].m_aValues; const ColorRGBA Color = ColorRGBA(fx2f(pValues[0]), fx2f(pValues[1]), fx2f(pValues[2]), fx2f(pValues[3])); const auto &&SetColor = [&](ColorRGBA NewColor) { - if(Color == NewColor) + if(Color == NewColor && pEditor->m_ColorPickerPopupContext.m_State == EEditState::EDITING) return; + + static int s_Values[4]; + + if(pEditor->m_ColorPickerPopupContext.m_State == EEditState::START || pEditor->m_ColorPickerPopupContext.m_State == EEditState::ONE_GO) + { + for(int Channel = 0; Channel < 4; ++Channel) + s_Values[Channel] = pValues[Channel]; + } + for(int Channel = 0; Channel < 4; ++Channel) + { pValues[Channel] = f2fx(NewColor[Channel]); + } + + if(pEditor->m_ColorPickerPopupContext.m_State == EEditState::END || pEditor->m_ColorPickerPopupContext.m_State == EEditState::ONE_GO) + { + std::vector> vpActions(4); + + for(int Channel = 0; Channel < 4; ++Channel) + { + vpActions[Channel] = std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, Channel, CEditorActionEnvelopeEditPoint::EEditType::VALUE, s_Values[Channel], f2fx(NewColor[Channel])); + } + + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Edit color of point %d of envelope %d", SelectedIndex, pEditor->m_SelectedEnvelope); + pEditor->m_EnvelopeEditorHistory.RecordAction(std::make_shared(pEditor, vpActions, aDisplay)); + } + pEditor->m_UpdateEnvPointInfo = true; pEditor->m_Map.OnModify(); }; @@ -1327,37 +1346,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie static CLineInputNumber s_CurValueInput; static CLineInputNumber s_CurTimeInput; + static float s_CurrentTime = 0; + static float s_CurrentValue = 0; + if(pEditor->m_UpdateEnvPointInfo) { pEditor->m_UpdateEnvPointInfo = false; - int CurrentTime; - int CurrentValue; - if(pEditor->IsTangentInSelected()) - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]; - CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel]; - } - else if(pEditor->IsTangentOutSelected()) - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]; - CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel]; - } - else - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); - - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time; - CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; - } + auto TimeAndValue = pEditor->EnvGetSelectedTimeAndValue(); + int CurrentTime = TimeAndValue.first; + int CurrentValue = TimeAndValue.second; // update displayed text s_CurValueInput.SetFloat(fx2f(CurrentValue)); s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); + + s_CurrentTime = s_CurTimeInput.GetFloat(); + s_CurrentValue = s_CurValueInput.GetFloat(); } View.HSplitTop(RowHeight, &Row, &View); @@ -1377,53 +1382,46 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie { float CurrentTime = s_CurTimeInput.GetFloat(); float CurrentValue = s_CurValueInput.GetFloat(); - if(pEditor->IsTangentInSelected()) + if(!(absolute(CurrentTime - s_CurrentTime) < 0.0001f && absolute(CurrentValue - s_CurrentValue) < 0.0001f)) { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; + auto [OldTime, OldValue] = pEditor->EnvGetSelectedTimeAndValue(); - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel] = minimum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[SelectedIndex].m_Time, 0); - CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]) / 1000.0f; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; - } - else if(pEditor->IsTangentOutSelected()) - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel] = maximum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[SelectedIndex].m_Time, 0); - CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]) / 1000.0f; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; - } - else - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); - if(pEnvelope->GetChannels() == 4) - CurrentValue = clamp(CurrentValue, 0.0f, 1.0f); - pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = f2fx(CurrentValue); - - if(SelectedIndex != 0) + if(pEditor->IsTangentInSelected()) { - pEnvelope->m_vPoints[SelectedIndex].m_Time = CurrentTime * 1000.0f; + auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - if(pEnvelope->m_vPoints[SelectedIndex].m_Time < pEnvelope->m_vPoints[SelectedIndex - 1].m_Time) - pEnvelope->m_vPoints[SelectedIndex].m_Time = pEnvelope->m_vPoints[SelectedIndex - 1].m_Time + 1; - if(static_cast(SelectedIndex) + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[SelectedIndex].m_Time > pEnvelope->m_vPoints[SelectedIndex + 1].m_Time) - pEnvelope->m_vPoints[SelectedIndex].m_Time = pEnvelope->m_vPoints[SelectedIndex + 1].m_Time - 1; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEditEnvelopePointValue::EType::TANGENT_IN, static_cast(OldTime * 1000.0f), f2fx(OldValue), static_cast(CurrentTime * 1000.0f), f2fx(CurrentValue))); + CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]) / 1000.0f; + } + else if(pEditor->IsTangentOutSelected()) + { + auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time / 1000.0f; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEditEnvelopePointValue::EType::TANGENT_OUT, static_cast(OldTime * 1000.0f), f2fx(OldValue), static_cast(CurrentTime * 1000.0f), f2fx(CurrentValue))); + CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]) / 1000.0f; } else { - CurrentTime = 0.0f; - pEnvelope->m_vPoints[SelectedIndex].m_Time = 0.0f; + auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEditEnvelopePointValue::EType::POINT, static_cast(OldTime * 1000.0f), f2fx(OldValue), static_cast(CurrentTime * 1000.0f), f2fx(CurrentValue))); + + if(SelectedIndex != 0) + { + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time / 1000.0f; + } + else + { + CurrentTime = 0.0f; + pEnvelope->m_vPoints[SelectedIndex].m_Time = 0.0f; + } } + + s_CurTimeInput.SetFloat(static_cast(CurrentTime * 1000.0f) / 1000.0f); + s_CurValueInput.SetFloat(fx2f(f2fx(CurrentValue))); + + s_CurrentTime = s_CurTimeInput.GetFloat(); + s_CurrentValue = s_CurValueInput.GetFloat(); } - - s_CurTimeInput.SetFloat(static_cast(CurrentTime * 1000.0f) / 1000.0f); - s_CurValueInput.SetFloat(fx2f(f2fx(CurrentValue))); - - pEditor->m_Map.OnModify(); } View.HSplitTop(6.0f, nullptr, &View); @@ -1436,24 +1434,18 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie if(pEditor->IsTangentInSelected()) { auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel] = 0.0f; - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel] = 0.0f; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, true)); } else if(pEditor->IsTangentOutSelected()) { auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel] = 0.0f; - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel] = 0.0f; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, false)); } else { auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); - - pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + SelectedIndex); + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex)); } - pEditor->m_Map.OnModify(); return CUI::POPUP_CLOSE_CURRENT; } @@ -1515,6 +1507,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CU if(pEditor->DoButton_MenuItem(&s_ButtonSmoothID, "Smooth", 0, &ButtonSmooth)) CurveType = CURVETYPE_SMOOTH; + std::vector> vpActions; + if(CurveType >= 0) { std::shared_ptr pEnvelope = pEditor->m_Map.m_vpEnvelopes.at(pEditor->m_SelectedEnvelope); @@ -1551,13 +1545,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CU CEnvPoint &CurrentPoint = pEnvelope->m_vPoints[SelectedIndex]; ColorRGBA Channels; HelperEnvelope.Eval(CurrentPoint.m_Time / 1000.0f, Channels); + int PrevValue = CurrentPoint.m_aValues[c]; CurrentPoint.m_aValues[c] = f2fx(Channels.r); + vpActions.push_back(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEnvelopeEditPoint::EEditType::VALUE, PrevValue, CurrentPoint.m_aValues[c])); } } } } } + if(!vpActions.empty()) + { + pEditor->m_EnvelopeEditorHistory.RecordAction(std::make_shared(pEditor, vpActions, "Project points")); + } + pEditor->m_Map.OnModify(); return CUI::POPUP_CLOSE_CURRENT; } diff --git a/src/game/editor/tileart.cpp b/src/game/editor/tileart.cpp index da96fdcff..5524dce02 100644 --- a/src/game/editor/tileart.cpp +++ b/src/game/editor/tileart.cpp @@ -1,4 +1,5 @@ #include "editor.h" +#include "editor_actions.h" #include @@ -184,10 +185,15 @@ static void SetTilelayerIndices(const std::shared_ptr &pLayer, cons } } -void CEditor::AddTileart() +void CEditor::AddTileart(bool IgnoreHistory) { + char aTileArtFileName[IO_MAX_PATH_LENGTH]; + IStorage::StripPathAndExtension(m_aTileartFilename, aTileArtFileName, sizeof(aTileArtFileName)); + std::shared_ptr pGroup = m_Map.NewGroup(); - str_copy(pGroup->m_aName, m_aTileartFilename); + str_copy(pGroup->m_aName, aTileArtFileName); + + int ImageCount = m_Map.m_vpImages.size(); auto vUniqueColors = GetUniqueColors(m_TileartImageInfo); auto vaColorGroups = GroupColors(vUniqueColors); @@ -195,11 +201,16 @@ void CEditor::AddTileart() char aImageName[IO_MAX_PATH_LENGTH]; for(size_t i = 0; i < vColorImages.size(); i++) { - str_format(aImageName, sizeof(aImageName), "%s %" PRIzu, m_aTileartFilename, i + 1); + str_format(aImageName, sizeof(aImageName), "%s %" PRIzu, aTileArtFileName, i + 1); std::shared_ptr pLayer = AddLayerWithImage(this, pGroup, m_TileartImageInfo.m_Width, m_TileartImageInfo.m_Height, vColorImages[i], aImageName); SetTilelayerIndices(pLayer, vaColorGroups[i], m_TileartImageInfo); } - SortImages(); + auto IndexMap = SortImages(); + + if(!IgnoreHistory) + { + m_EditorHistory.RecordAction(std::make_shared(this, ImageCount, m_aTileartFilename, IndexMap)); + } free(m_TileartImageInfo.m_pData); m_TileartImageInfo.m_pData = nullptr; @@ -237,7 +248,7 @@ bool CEditor::CallbackAddTileart(const char *pFilepath, int StorageType, void *p return false; } - IStorage::StripPathAndExtension(pFilepath, pEditor->m_aTileartFilename, sizeof(pEditor->m_aTileartFilename)); + str_copy(pEditor->m_aTileartFilename, pFilepath); if(pEditor->m_TileartImageInfo.m_Width * pEditor->m_TileartImageInfo.m_Height > 10'000) { pEditor->m_PopupEventType = CEditor::POPEVENT_PIXELART_BIG_IMAGE; diff --git a/src/game/mapitems.cpp b/src/game/mapitems.cpp index a9620bc98..2ccab3067 100644 --- a/src/game/mapitems.cpp +++ b/src/game/mapitems.cpp @@ -150,3 +150,13 @@ bool IsCreditsTile(int TileIndex) (TILE_CREDITS_7 == TileIndex) || (TILE_CREDITS_8 == TileIndex)); } + +int PackColor(CColor Color) +{ + int Res = 0; + Res |= Color.r << 24; + Res |= Color.g << 16; + Res |= Color.b << 8; + Res |= Color.a; + return Res; +} diff --git a/src/game/mapitems.h b/src/game/mapitems.h index c65fcfa88..d89cde0cc 100644 --- a/src/game/mapitems.h +++ b/src/game/mapitems.h @@ -578,5 +578,6 @@ bool IsValidTuneTile(int Index); bool IsValidEntity(int Index); bool IsRotatableTile(int Index); bool IsCreditsTile(int TileIndex); +int PackColor(CColor Color); #endif