Index: build/premake/premake5.lua =================================================================== --- build/premake/premake5.lua +++ build/premake/premake5.lua @@ -1115,6 +1115,7 @@ "CustomControls/FileHistory", "CustomControls/HighResTimer", "CustomControls/MapDialog", + "CustomControls/MapResizeDialog", "CustomControls/SnapSplitterWindow", "CustomControls/VirtualDirTreeCtrl", "CustomControls/Windows", Index: source/graphics/Terrain.h =================================================================== --- source/graphics/Terrain.h +++ source/graphics/Terrain.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -98,8 +98,9 @@ // should be in the direction (1,-1); false if it should be (1,1) bool GetTriangulationDir(ssize_t i, ssize_t j) const; - // resize this terrain such that each side has given number of patches - void Resize(ssize_t size); + // Resize this terrain such that each side has given number of patches, + // with the center offset in patches from the larger of the two sizes. + void ResizeRecenter(ssize_t size, int horizontalOffset = 0, int verticalOffset = 0); // set up a new heightmap from 16 bit data; assumes heightmap matches current terrain size void SetHeightMap(u16* heightmap); Index: source/graphics/Terrain.cpp =================================================================== --- source/graphics/Terrain.cpp +++ source/graphics/Terrain.cpp @@ -74,11 +74,11 @@ ReleaseData(); // store terrain size - m_MapSize = patchesPerSide*PATCH_SIZE+1; + m_MapSize = patchesPerSide * PATCH_SIZE + 1; m_MapSizePatches = patchesPerSide; // allocate data for new terrain - m_Heightmap = new u16[m_MapSize*m_MapSize]; - m_Patches = new CPatch[m_MapSizePatches*m_MapSizePatches]; + m_Heightmap = new u16[m_MapSize * m_MapSize]; + m_Patches = new CPatch[m_MapSizePatches * m_MapSizePatches]; // given a heightmap? if (data) @@ -496,98 +496,136 @@ return (mid1 < mid2); } -/////////////////////////////////////////////////////////////////////////////// -// Resize: resize this terrain to the given size (in patches per side) -void CTerrain::Resize(ssize_t size) +// Resize/Recenter: resize this terrain to the given size (in patches per side). +// The offset is in patches from the center of the source. +void CTerrain::ResizeRecenter(ssize_t size, i32 horizontalOffset, i32 verticalOffset) { - if (size==m_MapSizePatches) { - // inexplicable request to resize terrain to the same size .. ignore it + if (size == m_MapSizePatches && horizontalOffset == 0 && verticalOffset == 0) + { + // Inexplicable request to resize terrain to the same size .. ignore it return; } - if (!m_Heightmap) { - // not yet created a terrain; build a default terrain of the given size now - Initialize(size,0); + if (!m_Heightmap || + std::abs(horizontalOffset) > std::max(size, m_MapSizePatches) / 2 || + std::abs(verticalOffset) > std::max(size, m_MapSizePatches) / 2) + { + // We have not yet created a terrain, or we are offsetting outside the current source. + // Let's build a default terrain of the given size now. + Initialize(size, 0); return; } - // allocate data for new terrain - ssize_t newMapSize=size*PATCH_SIZE+1; - u16* newHeightmap=new u16[newMapSize*newMapSize]; - CPatch* newPatches=new CPatch[size*size]; - - if (size>m_MapSizePatches) { - // new map is bigger than old one - zero the heightmap so we don't get uninitialised - // height data along the expanded edges - memset(newHeightmap,0,newMapSize*newMapSize*sizeof(u16)); - } + // Allocate data for new terrain + ssize_t newMapSize = size * PATCH_SIZE + 1; + u16* newHeightmap = new u16[newMapSize * newMapSize]; + CPatch* newPatches = new CPatch[size * size]; - // now copy over rows of data - u16* src=m_Heightmap; - u16* dst=newHeightmap; - ssize_t copysize=std::min(newMapSize, m_MapSize); - for (ssize_t j=0;jm_MapSize) { - // extend the last height to the end of the row - for (size_t i=0;i(0), (size - m_MapSizePatches) / 2 + horizontalOffset); + const ssize_t upperLeftZ = std::max(static_cast(0), (size - m_MapSizePatches) / 2 + verticalOffset); + const ssize_t width = std::min(size, (size - m_MapSizePatches) / 2 + horizontalOffset + m_MapSizePatches) - upperLeftX; + const ssize_t height = std::min(size, (size - m_MapSizePatches) / 2 + verticalOffset + m_MapSizePatches) - upperLeftZ; + const ssize_t upperLeftXSource = std::max(static_cast(0), (m_MapSizePatches - size) / 2 - horizontalOffset); + const ssize_t upperLeftZSource = std::max(static_cast(0), (m_MapSizePatches - size) / 2 - verticalOffset); - if (newMapSize>m_MapSize) { - // copy over heights of the last row to any remaining rows - src=newHeightmap+((m_MapSize-1)*newMapSize); - dst=src+newMapSize; - for (ssize_t i=0;im_MapSizePatches) { - // copy over the last tile from each column - for (ssize_t n=0;nm_MapSizePatches) { - // copy over the last tile from each column - CPatch* srcpatch=&newPatches[(m_MapSizePatches-1)*size]; - CPatch* dstpatch=srcpatch+size; - for (ssize_t p=0;p<(ssize_t)size-m_MapSizePatches;p++) { - for (ssize_t n=0;n<(ssize_t)size;n++) { - for (ssize_t m=0;mm_MiniPatches[15][k]; - CMiniPatch& dst=dstpatch->m_MiniPatches[m][k]; - dst = src; - } + for (ssize_t j = 0; j < upperLeftZ; ++j) + { + CPatch* srcpatch = newPatches + (upperLeftZ * size); + CPatch* dstpatch = newPatches + j * size; + for (ssize_t i = 0; i < size; ++i) + { + for (ssize_t patch_j = 0; patch_j < PATCH_SIZE; ++patch_j) + { + for (ssize_t patch_i = 0; patch_i < PATCH_SIZE; ++patch_i) + { + CMiniPatch& src = srcpatch->m_MiniPatches[0][patch_i]; + CMiniPatch& dst = dstpatch->m_MiniPatches[patch_j][patch_i]; + dst = src; + } + } + ++srcpatch; + ++dstpatch; + } + } + for (ssize_t j = upperLeftZ + height; j < size; ++j) + { + CPatch* srcpatch = newPatches + ((upperLeftZ + height - 1) * size); + CPatch* dstpatch = newPatches + j * size; + for (ssize_t i = 0; i < size; ++i) + { + for (ssize_t patch_j = 0; patch_j < PATCH_SIZE; ++patch_j) + { + for (ssize_t patch_i = 0; patch_i < PATCH_SIZE; ++patch_i) + { + CMiniPatch& src = srcpatch->m_MiniPatches[PATCH_SIZE - 1][patch_i]; + CMiniPatch& dst = dstpatch->m_MiniPatches[patch_j][patch_i]; + dst = src; } - srcpatch++; - dstpatch++; } + ++srcpatch; + ++dstpatch; } } @@ -596,16 +634,16 @@ ReleaseData(); // store new data - m_Heightmap=newHeightmap; - m_Patches=newPatches; - m_MapSize=(ssize_t)newMapSize; - m_MapSizePatches=(ssize_t)size; + m_Heightmap = newHeightmap; + m_Patches = newPatches; + m_MapSize = newMapSize; + m_MapSizePatches = size; // initialise all the new patches InitialisePatches(); // initialise mipmap - m_HeightMipmap.Initialize(m_MapSize,m_Heightmap); + m_HeightMipmap.Initialize(m_MapSize, m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// Index: source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/MapResizeDialog.h =================================================================== --- /dev/null +++ source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/MapResizeDialog.h @@ -0,0 +1,49 @@ +/* Copyright (C) 2019 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#ifndef INCLUDED_MAPRESIZEDIALOG +#define INCLUDED_MAPRESIZEDIALOG + +#include "PseudoMiniMapPanel.h" +#include + +class MapResizeDialog : public wxDialog +{ +public: + MapResizeDialog(wxWindow* parent); + + /** + * Returns selected new size. + */ + ssize_t GetNewSize() const; + /** + * Returns the offset from center. + */ + wxPoint GetOffset() const; + +private: + void OnCancel(wxCommandEvent& evt); + void OnOK(wxCommandEvent& evt); + void OnListBox(wxCommandEvent& evt); + + ssize_t m_NewSize; + PseudoMiniMapPanel* m_MiniMap; + + DECLARE_EVENT_TABLE(); +}; + +#endif // INCLUDED_MAPRESIZEDIALOG Index: source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/MapResizeDialog.cpp =================================================================== --- /dev/null +++ source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/MapResizeDialog.cpp @@ -0,0 +1,118 @@ +/* Copyright (C) 2019 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#include "precompiled.h" + +#include "MapResizeDialog.h" +#include "ScenarioEditor/ScenarioEditor.h" + +#include "GameInterface/MessagePasser.h" +#include "GameInterface/Messages.h" + +#include + +MapResizeDialog::MapResizeDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCAPTION | wxRESIZE_BORDER) +{ + Freeze(); + + AtlasMessage::qGetCurrentMapSize qrySize; + qrySize.Post(); + m_NewSize = qrySize.size; + + SetTitle(_("Resize/Recenter map")); + wxSizer* sizer = new wxBoxSizer(wxVERTICAL); + wxStaticText* label = new wxStaticText(this, wxID_ANY, _("Select new map size and center."), wxDefaultPosition, wxDefaultSize); + sizer->Add(label, wxSizerFlags().Align(wxALIGN_CENTER_HORIZONTAL).Border(wxALL, 10)); + + wxBoxSizer* listAndMap = new wxBoxSizer(wxHORIZONTAL); + + wxListBox* listBox = new wxListBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE | wxLB_HSCROLL); + // Load the map sizes list + AtlasMessage::qGetMapSizes qrySizes; + qrySizes.Post(); + AtObj sizes = AtlasObject::LoadFromJSON(*qrySizes.sizes); + for (AtIter s = sizes["Data"]["item"]; s.defined(); ++s) + { + wxString size(s["Tiles"]); + listBox->Append(wxString(s["Name"]), new wxStringClientData(size)); + if (m_NewSize == static_cast(wxAtoi(size))) + listBox->SetSelection(listBox->GetCount() - 1); + } + listAndMap->Add(listBox, wxSizerFlags().Align(wxALIGN_LEFT).Proportion(1).Expand()); + listAndMap->AddSpacer(10); + + m_MiniMap = new PseudoMiniMapPanel(this, m_NewSize); + listBox->Bind(wxEVT_LISTBOX, &PseudoMiniMapPanel::OnNewSize, m_MiniMap); + + listAndMap->Add(m_MiniMap, wxSizerFlags()); + sizer->Add(listAndMap, wxSizerFlags().Proportion(1).Expand().Border(wxLEFT | wxRIGHT, 10)); + + sizer->AddSpacer(5); + sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL), wxSizerFlags().Expand().Border(wxRIGHT | wxLEFT, 7)); + sizer->AddSpacer(5); + + wxSizer* buttonSizer = new wxBoxSizer(wxHORIZONTAL); + buttonSizer->Add(new wxButton(this, wxID_OK, _("OK"))); + buttonSizer->AddSpacer(5); + buttonSizer->Add(new wxButton(this, wxID_CANCEL, _("Cancel"))); + + sizer->Add(buttonSizer, wxSizerFlags().Align(wxALIGN_RIGHT).Border(wxRIGHT | wxBOTTOM, 10)); + + SetSizerAndFit(sizer); + Layout(); + Thaw(); +} + +ssize_t MapResizeDialog::GetNewSize() const +{ + return m_NewSize; +} + +wxPoint MapResizeDialog::GetOffset() const +{ + return m_MiniMap->GetOffset(); +} + +void MapResizeDialog::OnListBox(wxCommandEvent& evt) +{ + if (!evt.IsSelection()) + return; + + m_NewSize = wxAtoi(static_cast(evt.GetClientObject())->GetData()); + if (evt.GetEventType() == wxEVT_COMMAND_LISTBOX_DOUBLECLICKED) + { + EndModal(wxID_OK); + } +} + +void MapResizeDialog::OnCancel(wxCommandEvent& WXUNUSED(evt)) +{ + EndModal(wxID_CANCEL); +} + +void MapResizeDialog::OnOK(wxCommandEvent& WXUNUSED(evt)) +{ + EndModal(wxID_OK); +} + +BEGIN_EVENT_TABLE(MapResizeDialog, wxDialog) + EVT_BUTTON(wxID_CANCEL, MapResizeDialog::OnCancel) + EVT_BUTTON(wxID_OK, MapResizeDialog::OnOK) + EVT_LISTBOX(wxID_ANY, MapResizeDialog::OnListBox) + EVT_LISTBOX_DCLICK(wxID_ANY, MapResizeDialog::OnListBox) +END_EVENT_TABLE() Index: source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/PseudoMiniMapPanel.h =================================================================== --- /dev/null +++ source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/PseudoMiniMapPanel.h @@ -0,0 +1,56 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + + +#ifndef INCLUDED_PSEUDOMINIMAPPANEL +#define INCLUDED_PSEUDOMINIMAPPANEL + +#include + +class PseudoMiniMapPanel : public wxPanel +{ +public: + PseudoMiniMapPanel(wxWindow* parent, int currentSize); + + void PaintEvent(wxPaintEvent& evt); + void EraseBackground(wxEraseEvent& evt); + + void OnNewSize(wxCommandEvent& evt); + + wxPoint GetOffset() const; +private: + void OnMouseDown(wxMouseEvent& evt); + void OnMouseUp(wxMouseEvent& evt); + void OnMouseMove(wxMouseEvent& evt); + void OnMouseLeave(wxMouseEvent& evt); + + const int m_CurrentSize; + wxImage m_Background; + std::map m_ScreenTones; + std::map m_Backgrounds; + + wxPoint m_LastMousePos; + bool m_Dragging; + wxPoint m_SelectionCenter; + int m_SelectionRadius; + bool m_SameOrGrowing; + int m_NewSize; + + DECLARE_EVENT_TABLE(); +}; + +#endif // INCLUDED_PSEUDOMINIMAPPANEL Index: source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/PseudoMiniMapPanel.cpp =================================================================== --- /dev/null +++ source/tools/atlas/AtlasUI/CustomControls/MapResizeDialog/PseudoMiniMapPanel.cpp @@ -0,0 +1,232 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "PseudoMiniMapPanel.h" + +#include "GameInterface/MessagePasser.h" +#include "GameInterface/Messages.h" +#include "ScenarioEditor/Tools/Common/Tools.h" + +#include +#include +#include +#include +#include + +namespace +{ + const int PanelRadius = 64 + 1; + const wxPoint PanelCenter = wxPoint(PanelRadius + 1, PanelRadius + 1); + const wxPoint ScreenToneOffset(-2 * PanelRadius, -2 * PanelRadius); + const wxPen Rim(*wxBLACK, 3); + const wxPen BackgroundMask(*wxBLACK, 2 * PanelRadius); + + bool Within(const wxPoint& test, const wxPoint& center, int radius) + { + int dx = abs(test.x - center.x); + if (dx > radius) + return false; + int dy = abs(test.y - center.y); + if (dy > radius) + return false; + if (dx + dy <= radius) + return true; + return (dx * dx + dy * dy <= radius * radius); + } +} + +PseudoMiniMapPanel::PseudoMiniMapPanel(wxWindow* parent, int currentSize) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(PanelRadius * 2 + 1, PanelRadius * 2 + 1)), + m_CurrentSize(currentSize), m_ScreenTones(), + m_LastMousePos(-1, -1), m_Dragging(false), + m_SelectionRadius(PanelRadius), m_SelectionCenter(PanelCenter), m_SameOrGrowing(true), m_NewSize(currentSize) +{ + + AtlasMessage::qRasterizeMinimap qryBackground; + qryBackground.Post(); + int dim = qryBackground.dimension; + std::vector imageBytes = *qryBackground.imageBytes; + + // Data is destined for a wxImage, which uses free. + uint8_t* data = static_cast(malloc(imageBytes.size())); + std::copy(imageBytes.cbegin(), imageBytes.cend(), data); + m_Background = wxImage(dim, dim, data); + m_Background.Rescale(PanelRadius * 2, PanelRadius * 2, wxIMAGE_QUALITY_BOX_AVERAGE); + m_Backgrounds[PanelRadius] = wxBitmap(m_Background); + + SetBackgroundStyle(wxBG_STYLE_PAINT); +} + +wxPoint PseudoMiniMapPanel::GetOffset() const +{ + // Since offset is from center, amplitude is (at most) half the largest size. + int size = std::max(m_CurrentSize, m_NewSize) / 2; + // If the map is growing, the display is opposite what the actual offset is. + float scalar = (m_SameOrGrowing ? 1.0 : -1.0) / PanelRadius * size; + // Rebase offsets to center. + int hOffset = m_SelectionCenter.x - PanelCenter.x; + int vOffset = m_SelectionCenter.y - PanelCenter.y; + return wxPoint(scalar * hOffset, scalar * vOffset); +} + +void PseudoMiniMapPanel::OnNewSize(wxCommandEvent& evt) +{ + if (!evt.IsSelection()) + return; + + evt.Skip(); + + m_NewSize = wxAtoi(static_cast(evt.GetClientObject())->GetData()); + + m_SameOrGrowing = m_NewSize >= m_CurrentSize; + m_SelectionRadius = std::min(m_NewSize, m_CurrentSize) * PanelRadius / std::max(m_NewSize, m_CurrentSize); + if (!m_SameOrGrowing && m_ScreenTones.find(m_SelectionRadius) == m_ScreenTones.cend()) + { + wxImage overlay = wxImage(PanelRadius * 4, PanelRadius * 4); + overlay.InitAlpha(); + wxGraphicsContext* gc = wxGraphicsContext::Create(overlay); + gc->SetBrush(*wxGREY_BRUSH); + gc->DrawRectangle(0, 0, PanelRadius * 4, PanelRadius * 4); + gc->SetBrush(*wxBLACK_BRUSH); + gc->DrawEllipse(PanelRadius * 2 - m_SelectionRadius, PanelRadius * 2 - m_SelectionRadius, m_SelectionRadius * 2, m_SelectionRadius * 2); + gc->SetPen(*wxWHITE_PEN); + gc->DrawEllipse(PanelRadius * 2 - m_SelectionRadius, PanelRadius * 2 - m_SelectionRadius, m_SelectionRadius * 2, m_SelectionRadius * 2); + delete gc; + // Black -> Converted to transparent. + // White -> converted to black. + overlay.ConvertColourToAlpha(0, 0, 0); + + m_ScreenTones[m_SelectionRadius] = wxBitmap(overlay); + } + else if (m_SameOrGrowing && m_Backgrounds.find(m_SelectionRadius) == m_Backgrounds.cend()) + { + wxImage rescaled = wxImage(m_Background); + rescaled.Rescale(2 * m_SelectionRadius, 2 * m_SelectionRadius, wxIMAGE_QUALITY_BOX_AVERAGE); + m_Backgrounds[m_SelectionRadius] = wxBitmap(rescaled); + } + + Refresh(); +} + +void PseudoMiniMapPanel::OnMouseDown(wxMouseEvent& evt) +{ + // Capture on button-down, so we can respond even when the mouse + // moves off the window + if (!m_Dragging && evt.ButtonDown() && + Within(evt.GetPosition(), PanelCenter, PanelRadius) && + Within(evt.GetPosition(), m_SelectionCenter, m_SelectionRadius)) + { + m_LastMousePos = evt.GetPosition(); + m_Dragging = true; + } +} + +void PseudoMiniMapPanel::OnMouseUp(wxMouseEvent& evt) +{ + if (m_Dragging && + !(evt.ButtonIsDown(wxMOUSE_BTN_LEFT) || evt.ButtonIsDown(wxMOUSE_BTN_MIDDLE) || evt.ButtonIsDown(wxMOUSE_BTN_RIGHT)) + ) + { + m_Dragging = false; + } +} + +void PseudoMiniMapPanel::OnMouseMove(wxMouseEvent& evt) +{ + if (m_Dragging && evt.Dragging()) + { + if (m_LastMousePos == evt.GetPosition()) + return; + + wxPoint delta = evt.GetPosition() - m_LastMousePos; + wxPoint moved = m_SelectionCenter + delta; + + if (!Within(moved, PanelCenter, PanelRadius)) + return; + + m_SelectionCenter = moved; + m_LastMousePos = evt.GetPosition(); + Refresh(); + } +} + +void PseudoMiniMapPanel::OnMouseLeave(wxMouseEvent& WXUNUSED(evt)) +{ + m_Dragging = false; +} + +void PseudoMiniMapPanel::PaintEvent(wxPaintEvent& WXUNUSED(evt)) +{ + wxAutoBufferedPaintDC dca(this); + // Background must be grabbed from paint dc, not gc, or color may be transparent. + wxColor background = dca.GetBackground().GetColour(); + wxGCDC dc(dca); + if (m_SameOrGrowing) + { + dc.DrawBitmap(m_Backgrounds[m_SelectionRadius], m_SelectionCenter - wxSize(m_SelectionRadius, m_SelectionRadius)); + + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(BackgroundMask); + dc.DrawCircle(m_SelectionCenter, PanelRadius + m_SelectionRadius); + + const wxPen BorderPen(*wxWHITE, 2); + dc.SetPen(BorderPen); + dc.DrawCircle(m_SelectionCenter, m_SelectionRadius); + } + else + { + dc.DrawBitmap(m_Backgrounds[PanelRadius], 0, 0); + // "fade out" trimmed areas by drawing a screentone ring ring. + dc.DrawBitmap(m_ScreenTones[m_SelectionRadius], ScreenToneOffset + m_SelectionCenter); + } + + // Centering markers. + dc.SetBrush(*wxBLACK_BRUSH); + dc.SetPen(*wxBLACK_PEN); + dc.DrawCircle(m_SelectionCenter, 2); + dc.SetPen(*wxWHITE_PEN); + dc.DrawLine(PanelRadius - 10, PanelRadius, PanelRadius + 10, PanelRadius); + dc.DrawLine(PanelRadius, PanelRadius + 10, PanelRadius, PanelRadius - 10); + + // Round border. + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(Rim); + dc.DrawCircle(PanelCenter, PanelRadius - 1); + wxPen mask(background, PanelRadius); + dc.SetPen(mask); + dc.DrawCircle(PanelCenter, PanelRadius + PanelRadius / 2 - 1); +} + +void PseudoMiniMapPanel::EraseBackground(wxEraseEvent& WXUNUSED(evt)) +{ + // Do nothing - don't erase to remove flicker. +} + +BEGIN_EVENT_TABLE(PseudoMiniMapPanel, wxPanel) + EVT_LEAVE_WINDOW(PseudoMiniMapPanel::OnMouseUp) + EVT_LEFT_DOWN(PseudoMiniMapPanel::OnMouseDown) + EVT_LEFT_UP(PseudoMiniMapPanel::OnMouseUp) + EVT_RIGHT_DOWN(PseudoMiniMapPanel::OnMouseDown) + EVT_RIGHT_UP(PseudoMiniMapPanel::OnMouseUp) + EVT_MIDDLE_DOWN(PseudoMiniMapPanel::OnMouseDown) + EVT_MIDDLE_UP(PseudoMiniMapPanel::OnMouseUp) + EVT_MOTION(PseudoMiniMapPanel::OnMouseMove) + EVT_LEAVE_WINDOW(PseudoMiniMapPanel::OnMouseLeave) + EVT_PAINT(PseudoMiniMapPanel::PaintEvent) +END_EVENT_TABLE() Index: source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.h =================================================================== --- source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.h +++ source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -41,6 +41,7 @@ void OnRandomReseed(wxCommandEvent& evt); void OnRandomGenerate(wxCommandEvent& evt); void OnOpenPlayerPanel(wxCommandEvent& evt); + void OnResizeMap(wxCommandEvent& evt); void UpdateSimButtons(); int m_SimState; Index: source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp =================================================================== --- source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp +++ source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp @@ -21,6 +21,7 @@ #include "AtlasObject/AtlasObject.h" #include "GameInterface/Messages.h" +#include "MapResizeDialog/MapResizeDialog.h" #include "ScenarioEditor/ScenarioEditor.h" #include "ScenarioEditor/Tools/Common/Tools.h" @@ -60,7 +61,8 @@ ID_SimSlow, ID_SimPause, ID_SimReset, - ID_OpenPlayerPanel + ID_OpenPlayerPanel, + ID_ResizeMap }; enum @@ -410,6 +412,15 @@ _("Run selected random map script")), wxSizerFlags().Expand()); } + { + + ///////////////////////////////////////////////////////////////////////// + // Misc tools + wxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Misc tools")); + sizer->Add(new wxButton(scrolledWindow, ID_ResizeMap, _("Resize/Recenter map")), wxSizerFlags().Expand()); + scrollSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10)); + } + { ///////////////////////////////////////////////////////////////////////// // Simulation buttons @@ -645,6 +656,16 @@ m_ScenarioEditor.SelectPage(_T("PlayerSidebar")); } +void MapSidebar::OnResizeMap(wxCommandEvent& WXUNUSED(evt)) +{ + MapResizeDialog dlg(this); + + if (dlg.ShowModal() != wxID_OK) + return; + wxPoint offset = dlg.GetOffset(); + POST_COMMAND(ResizeMap, (dlg.GetNewSize(), offset.x, offset.y)); +} + BEGIN_EVENT_TABLE(MapSidebar, Sidebar) EVT_COLLAPSIBLEPANE_CHANGED(wxID_ANY, MapSidebar::OnCollapse) EVT_BUTTON(ID_SimPlay, MapSidebar::OnSimPlay) @@ -655,4 +676,5 @@ EVT_BUTTON(ID_RandomReseed, MapSidebar::OnRandomReseed) EVT_BUTTON(ID_RandomGenerate, MapSidebar::OnRandomGenerate) EVT_BUTTON(ID_OpenPlayerPanel, MapSidebar::OnOpenPlayerPanel) + EVT_BUTTON(ID_ResizeMap, MapSidebar::OnResizeMap) END_EVENT_TABLE(); Index: source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h =================================================================== --- source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h +++ source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -30,11 +30,9 @@ private: void OnPassabilityChoice(wxCommandEvent& evt); void OnShowPriorities(wxCommandEvent& evt); - void OnResizeMap(wxCommandEvent& evt); wxChoice* m_PassabilityChoice; TexturePreviewPanel* m_TexturePreview; DECLARE_EVENT_TABLE(); }; - Index: source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp =================================================================== --- source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp +++ source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Terrain/Terrain.cpp @@ -47,8 +47,7 @@ enum { ID_Passability = 1, - ID_ShowPriorities, - ID_ResizeMap + ID_ShowPriorities }; // Helper function for adding tooltips @@ -247,14 +246,6 @@ _("Show terrain texture priorities"))); } - { - ///////////////////////////////////////////////////////////////////////// - // Misc tools - wxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Misc tools")); - sizer->Add(new wxButton(scrolledWindow, ID_ResizeMap, _("Resize map")), wxSizerFlags().Expand()); - scrollSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10)); - } - m_BottomBar = new TerrainBottomBar(scenarioEditor, bottomBarContainer); } @@ -283,37 +274,9 @@ POST_MESSAGE(SetViewParamB, (AtlasMessage::eRenderView::GAME, L"priorities", evt.IsChecked())); } -void TerrainSidebar::OnResizeMap(wxCommandEvent& WXUNUSED(evt)) -{ - wxArrayString sizeNames; - std::vector sizeTiles; - - // Load the map sizes list - AtlasMessage::qGetMapSizes qrySizes; - qrySizes.Post(); - AtObj sizes = AtlasObject::LoadFromJSON(*qrySizes.sizes); - for (AtIter s = sizes["Data"]["item"]; s.defined(); ++s) - { - sizeNames.Add(wxString::FromUTF8(s["Name"])); - sizeTiles.push_back((*s["Tiles"]).getLong()); - } - - // TODO: set default based on current map size - - wxSingleChoiceDialog dlg(this, _("Select new map size. WARNING: This probably only works reliably on blank maps."), - _("Resize map"), sizeNames); - - if (dlg.ShowModal() != wxID_OK) - return; - - size_t tiles = sizeTiles.at(dlg.GetSelection()); - POST_COMMAND(ResizeMap, (tiles)); -} - BEGIN_EVENT_TABLE(TerrainSidebar, Sidebar) EVT_CHOICE(ID_Passability, TerrainSidebar::OnPassabilityChoice) EVT_CHECKBOX(ID_ShowPriorities, TerrainSidebar::OnShowPriorities) - EVT_BUTTON(ID_ResizeMap, TerrainSidebar::OnResizeMap) END_EVENT_TABLE(); ////////////////////////////////////////////////////////////////////////// @@ -476,7 +439,7 @@ void LoadTerrain() { - wxBusyInfo busy (_("Loading terrain groups")); + wxBusyInfo busy(_("Loading terrain groups")); DeleteAllPages(); m_TerrainGroups.Clear(); Index: source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp =================================================================== --- source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp +++ source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp @@ -18,8 +18,9 @@ #include "precompiled.h" #include "MessageHandler.h" -#include "../GameLoop.h" #include "../CommandProc.h" +#include "../GameLoop.h" +#include "../MessagePasser.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" @@ -36,16 +37,21 @@ #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" +#include "ps/GameSetup/GameSetup.h" #include "ps/Loader.h" #include "ps/World.h" #include "renderer/Renderer.h" +#include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" +#include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerrain.h" +#include "simulation2/system/ParamNode.h" namespace { @@ -191,7 +197,7 @@ // Notice that the number of tiles/pixels per side of the heightmap image is // one less than the number of vertices per side of the heightmap. CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - terrain->Resize((sqrt(heightmap_source.size()) - 1) / PATCH_SIZE); + terrain->ResizeRecenter((sqrt(heightmap_source.size()) - 1) / PATCH_SIZE); // copy heightmap data into map u16* heightmap = g_Game->GetWorld()->GetTerrain()->GetHeightMap(); @@ -269,19 +275,134 @@ msg->sizes = g_Game->GetSimulation2()->GetMapSizes(); } +QUERYHANDLER(RasterizeMinimap) +{ + const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); + const ssize_t dimension = terrain->GetVerticesPerSide() - 1; + const ssize_t bpp = 24; + const ssize_t buf_size = dimension * dimension * (bpp / 8); + + std::vector imageBytes = std::vector(buf_size); + + // Stolen from MiniMap.cpp + float shallowPassageHeight = 0.0f; + // Get the maximum height for unit passage in water. + CParamNode externalParamNode; + CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); + const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses"); + if (pathingSettings.GetChild("default").IsOk() && pathingSettings.GetChild("default").GetChild("MaxWaterDepth").IsOk()) + shallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat(); + + ssize_t w = dimension; + ssize_t h = dimension; + float waterHeight = g_Renderer.GetWaterManager()->m_WaterHeight; + + for (ssize_t j = 0; j < h; ++j) + { + // Work backwards to vertically flip the image. + ssize_t position = 3 * (h - j - 1) * dimension; + for (ssize_t i = 0; i < w; ++i) + { + float avgHeight = (terrain->GetVertexGroundLevel(i, j) + + terrain->GetVertexGroundLevel(i + 1, j) + + terrain->GetVertexGroundLevel(i, j + 1) + + terrain->GetVertexGroundLevel(i + 1, j + 1) + ) / 4.0f; + + if (avgHeight < waterHeight && avgHeight > waterHeight - shallowPassageHeight) + { + // shallow water + imageBytes[position++] = 0x70; + imageBytes[position++] = 0x98; + imageBytes[position++] = 0xc0; + } + else if (avgHeight < waterHeight) + { + // Set water as constant color for consistency on different maps + imageBytes[position++] = 0x50; + imageBytes[position++] = 0x78; + imageBytes[position++] = 0xa0; + } + else + { + u32 color = std::numeric_limits::max(); + u32 hmap = static_cast(terrain->GetHeightMap()[j * dimension + i]) >> 8; + float scale = hmap / 3.0f + 170.0f / 255.0f; + + CMiniPatch* mp = terrain->GetTile(i, j); + if (mp) + { + CTerrainTextureEntry* tex = mp->GetTextureEntry(); + if (tex) + color = tex->GetBaseColor(); + } + + // Convert + imageBytes[position++] = static_cast(static_cast(color & 0xff) * scale); + imageBytes[position++] = static_cast(static_cast((color >> 8) & 0xff) * scale); + imageBytes[position++] = static_cast(static_cast((color >> 16) & 0xff) * scale); + } + } + } + + msg->imageBytes = imageBytes; + msg->dimension = dimension; +} + QUERYHANDLER(GetRMSData) { msg->data = g_Game->GetSimulation2()->GetRMSData(); } +QUERYHANDLER(GetCurrentMapSize) +{ + msg->size = g_Game->GetWorld()->GetTerrain()->GetTilesPerSide(); +} + BEGIN_COMMAND(ResizeMap) { - int m_OldTiles, m_NewTiles; + bool Within(const CFixedVector3D& pos, const int centerX, const int centerZ, const int radius) + { + int dx = abs(pos.X.ToInt_RoundToZero() - centerX); + if (dx > radius) + return false; + int dz = abs(pos.Z.ToInt_RoundToZero() - centerZ); + if (dz > radius) + return false; + if (dx + dz <= radius) + return true; + return (dx * dx + dz * dz <= radius * radius); + } + + struct DeletedObject + { + entity_id_t entityId; + CStr templateName; + int32_t owner; + CFixedVector3D pos; + CFixedVector3D rot; + }; + + ssize_t m_OldPatches, m_NewPatches; + int m_OffsetX, m_OffsetY; + + u16* m_Heightmap; + CPatch* m_Patches; + + std::vector m_DeletedObjects; + std::vector> m_OldPositions; + std::vector> m_NewPositions; cResizeMap() { } + ~cResizeMap() + { + delete m_Heightmap; + delete m_Patches; + } + void MakeDirty() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); @@ -293,39 +414,186 @@ g_Game->GetView()->GetLOSTexture().MakeDirty(); } - void ResizeTerrain(int tiles) + + void ResizeTerrain(ssize_t patches, int offsetX, int offsetY) { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); + terrain->ResizeRecenter(patches, offsetX, offsetY); + } + + void DeleteAll(const std::vector& deletedObjects) + { + for (const DeletedObject& deleted : deletedObjects) + { + g_Game->GetSimulation2()->DestroyEntity(deleted.entityId); + } - terrain->Resize(tiles / PATCH_SIZE); + g_Game->GetSimulation2()->FlushDestroyedEntities(); + } - MakeDirty(); + void UndeleteAll(const std::vector& deletedObjects) + { + CSimulation2& sim = *g_Game->GetSimulation2(); + + for (const DeletedObject& deleted : deletedObjects) + { + entity_id_t ent = sim.AddEntity(deleted.templateName.FromUTF8(), deleted.entityId); + if (ent == INVALID_ENTITY) + { + LOGERROR("Failed to load entity template '%s'", deleted.templateName.c_str()); + } + else + { + CmpPtr cmpPosition(sim, deleted.entityId); + if (cmpPosition) + { + cmpPosition->JumpTo(deleted.pos.X, deleted.pos.Z); + cmpPosition->SetXZRotation(deleted.rot.X, deleted.rot.Z); + cmpPosition->SetYRotation(deleted.rot.Y); + } + + CmpPtr cmpOwnership(sim, deleted.entityId); + if (cmpOwnership) + cmpOwnership->SetOwner(deleted.owner); + } + } + } + + void SetMovedEntitiesPosition(const std::vector>& movedObjects) + { + for (std::pair const& kv : movedObjects) + { + entity_id_t id = kv.first; + CFixedVector3D position = kv.second; + CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); + ENSURE(cmpPosition); + cmpPosition->JumpTo(position.X, position.Z); + } } void Do() { - CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); + CSimulation2& sim = *g_Game->GetSimulation2(); + CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); + ENSURE(cmpTemplateManager); + + CmpPtr cmpTerrain(sim, SYSTEM_ENTITY); if (!cmpTerrain) { - m_OldTiles = m_NewTiles = 0; + m_OldPatches = m_NewPatches = 0; + m_OffsetX = m_OffsetY = 0; } else { - m_OldTiles = (int)cmpTerrain->GetTilesPerSide(); - m_NewTiles = msg->tiles; + m_OldPatches = static_cast(cmpTerrain->GetTilesPerSide() / PATCH_SIZE); + m_NewPatches = msg->tiles / PATCH_SIZE; + m_OffsetX = msg->offsetX / PATCH_SIZE; + // Need to flip direction of vertical offset, due to screen mapping order. + m_OffsetY = -(msg->offsetY / PATCH_SIZE); + + CTerrain* terrain = cmpTerrain->GetCTerrain(); + m_Heightmap = new u16[(m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1)]; + std::copy_n(terrain->GetHeightMap(), (m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1), m_Heightmap); + m_Patches = new CPatch[m_OldPatches * m_OldPatches]; + for (ssize_t j = 0; j < m_OldPatches; ++j) + { + for (ssize_t i = 0; i < m_OldPatches; ++i) + { + CPatch& src = *(terrain->GetPatch(i, j)); + CPatch& dst = m_Patches[j * m_OldPatches + i]; + std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); + } + } } - ResizeTerrain(m_NewTiles); + const int radiusInTerrainUnits = m_NewPatches * PATCH_SIZE * TERRAIN_TILE_SIZE / 2 * (1.f - 1e-6f); + // Opposite direction offset, as we move the destination onto the source, not the source into the destination. + const int mapCenterX = (m_OldPatches / 2 - m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE; + const int mapCenterZ = (m_OldPatches / 2 - m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE; + // The offset to move units by is opposite the direction the map is moved, and from the corner. + const int offsetX = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE; + const int offsetZ = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE; + const CFixedVector3D offset = CFixedVector3D(fixed::FromInt(offsetX), fixed::FromInt(0), fixed::FromInt(offsetZ)); + + const CSimulation2::InterfaceListUnordered& ents = sim.GetEntitiesWithInterfaceUnordered(IID_Selectable); + + for (std::pair const& kv : ents) + { + const entity_id_t entityId = kv.first; + + CmpPtr cmpPosition(sim, entityId); + + if (cmpPosition && cmpPosition->IsInWorld() && Within(cmpPosition->GetPosition(), mapCenterX, mapCenterZ, radiusInTerrainUnits)) + { + CFixedVector3D position = cmpPosition->GetPosition(); + + m_NewPositions.emplace_back(entityId, position + offset); + m_OldPositions.emplace_back(entityId, position); + } + else + { + DeletedObject deleted; + deleted.entityId = entityId; + deleted.templateName = cmpTemplateManager->GetCurrentTemplateName(entityId); + + // If the entity has a position, but the ending position is not valid; + if (cmpPosition) + { + deleted.pos = cmpPosition->GetPosition(); + deleted.rot = cmpPosition->GetRotation(); + } + + CmpPtr cmpOwnership(sim, entityId); + if (cmpOwnership) + deleted.owner = cmpOwnership->GetOwner(); + + m_DeletedObjects.push_back(deleted); + } + } + + DeleteAll(m_DeletedObjects); + ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY); + SetMovedEntitiesPosition(m_NewPositions); + MakeDirty(); } + void Undo() { - ResizeTerrain(m_OldTiles); + if (m_Heightmap == nullptr || m_Patches == nullptr) + { + // If there previously was no data, just resize to old (probably not originally valid). + ResizeTerrain(m_OldPatches, -m_OffsetX, -m_OffsetY); + } + else + { + CSimulation2& sim = *g_Game->GetSimulation2(); + CmpPtr cmpTerrain(sim, SYSTEM_ENTITY); + CTerrain* terrain = cmpTerrain->GetCTerrain(); + + terrain->Initialize(m_OldPatches, m_Heightmap); + // Copy terrain data back. + for (ssize_t j = 0; j < m_OldPatches; ++j) + { + for (ssize_t i = 0; i < m_OldPatches; ++i) + { + CPatch& src = m_Patches[j * m_OldPatches + i]; + CPatch& dst = *(terrain->GetPatch(i, j)); + std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); + } + } + } + UndeleteAll(m_DeletedObjects); + SetMovedEntitiesPosition(m_OldPositions); + MakeDirty(); } void Redo() { - ResizeTerrain(m_NewTiles); + DeleteAll(m_DeletedObjects); + ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY); + SetMovedEntitiesPosition(m_NewPositions); + MakeDirty(); } }; END_COMMAND(ResizeMap) Index: source/tools/atlas/GameInterface/Messages.h =================================================================== --- source/tools/atlas/GameInterface/Messages.h +++ source/tools/atlas/GameInterface/Messages.h @@ -195,6 +195,17 @@ ((std::string, sizes)) ); +QUERY(GetCurrentMapSize, + , + ((int, size)) + ); + +QUERY(RasterizeMinimap, + , + ((int, dimension)) + ((std::vector, imageBytes)) + ); + QUERY(GetRMSData, , ((std::vector, data)) @@ -202,6 +213,8 @@ COMMAND(ResizeMap, NOMERGE, ((int, tiles)) + ((int, offsetX)) + ((int, offsetY)) ); QUERY(VFSFileExists,