From 946be50807c3bca6044749bda4b6514f295b24ba Mon Sep 17 00:00:00 2001 From: marmare314 <49279081+Marmare314@users.noreply.github.com> Date: Sat, 12 Aug 2023 12:13:27 +0200 Subject: [PATCH] Add tileart tool to editor --- CMakeLists.txt | 1 + src/game/editor/editor.cpp | 2 +- src/game/editor/editor.h | 11 +- src/game/editor/popups.cpp | 41 +++++- src/game/editor/tileart.cpp | 264 ++++++++++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 src/game/editor/tileart.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e18f2634..53fed51b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2325,6 +2325,7 @@ if(CLIENT) proof_mode.h smooth_value.cpp smooth_value.h + tileart.cpp ) set(GAME_GENERATED_CLIENT src/game/generated/checksum.cpp diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index bff0bfd14..4283d79fb 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -7208,7 +7208,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar) if(DoButton_Menu(&s_ToolsButton, "Tools", 0, &ToolsButton, 0, nullptr)) { static SPopupMenuId s_PopupMenuToolsId; - UI()->DoPopupMenu(&s_PopupMenuToolsId, ToolsButton.x, ToolsButton.y + ToolsButton.h - 1.0f, 200.0f, 50.0f, this, PopupMenuTools, PopupProperties); + UI()->DoPopupMenu(&s_PopupMenuToolsId, ToolsButton.x, ToolsButton.y + ToolsButton.h - 1.0f, 200.0f, 64.0f, this, PopupMenuTools, PopupProperties); } MenuBar.VSplitLeft(5.0f, nullptr, &MenuBar); diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 2773657b6..29ec69a34 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -1046,7 +1046,10 @@ public: POPEVENT_PREVENTUNUSEDTILES, POPEVENT_IMAGEDIV16, POPEVENT_IMAGE_MAX, - POPEVENT_PLACE_BORDER_TILES + POPEVENT_PLACE_BORDER_TILES, + POPEVENT_PIXELART_BIG_IMAGE, + POPEVENT_PIXELART_MANY_COLORS, + POPEVENT_PIXELART_TOO_MANY_COLORS }; int m_PopupEventType; @@ -1265,6 +1268,11 @@ public: CLineInputBuffered<256> m_SettingsCommandInput; + CImageInfo m_TileartImageInfo; + char m_aTileartFilename[IO_MAX_PATH_LENGTH]; + void AddTileart(); + void TileartCheckColors(); + void PlaceBorderTiles(); void UpdateTooltip(const void *pID, const CUIRect *pRect, const char *pToolTip); @@ -1327,6 +1335,7 @@ public: static bool CallbackAppendMap(const char *pFileName, int StorageType, void *pUser); static bool CallbackSaveMap(const char *pFileName, int StorageType, void *pUser); static bool CallbackSaveCopyMap(const char *pFileName, int StorageType, void *pUser); + static bool CallbackAddTileart(const char *pFilepath, int StorageType, void *pUser); void PopupSelectImageInvoke(int Current, float x, float y); int PopupSelectImageResult(); diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 0b9ac96b1..e175cdede 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -198,6 +198,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuTools(void *pContext, CUIRect Vi pEditor->UI()->DoPopupMenu(&s_PopupGotoId, Slot.x, Slot.y + Slot.h, 120, 52, pEditor, PopupGoto); } + static int s_TileartButton = 0; + View.HSplitTop(2.0f, nullptr, &View); + View.HSplitTop(12.0f, &Slot, &View); + if(pEditor->DoButton_MenuItem(&s_TileartButton, "Add tileart", 0, &Slot, 0, "Generate tileart from image")) + { + pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Add tileart", "Open", "mapres", false, CallbackAddTileart, pEditor); + return CUI::POPUP_CLOSE_CURRENT; + } + return CUI::POPUP_KEEP_OPEN; } @@ -1764,6 +1773,21 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, pTitle = "Place border tiles"; pMessage = "This is going to overwrite any existing tiles around the edges of the layer.\n\nContinue?"; } + else if(pEditor->m_PopupEventType == POPEVENT_PIXELART_BIG_IMAGE) + { + pTitle = "Big image"; + pMessage = "The selected image is big. Converting it to tileart may take some time.\n\nContinue anyway?"; + } + else if(pEditor->m_PopupEventType == POPEVENT_PIXELART_MANY_COLORS) + { + pTitle = "Many colors"; + pMessage = "The selected image contains many colors, which will lead to a big mapfile. You may want to consider reducing the number of colors.\n\nContinue anyway?"; + } + else if(pEditor->m_PopupEventType == POPEVENT_PIXELART_TOO_MANY_COLORS) + { + pTitle = "Too many colors"; + pMessage = "The client only supports 64 images but more would be needed to add the selected image as tileart."; + } else { dbg_assert(false, "m_PopupEventType invalid"); @@ -1786,12 +1810,19 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, // button bar ButtonBar.VSplitLeft(110.0f, &Button, &ButtonBar); - if(pEditor->m_PopupEventType != POPEVENT_LARGELAYER && pEditor->m_PopupEventType != POPEVENT_PREVENTUNUSEDTILES && pEditor->m_PopupEventType != POPEVENT_IMAGEDIV16 && pEditor->m_PopupEventType != POPEVENT_IMAGE_MAX) + if(pEditor->m_PopupEventType != POPEVENT_LARGELAYER && pEditor->m_PopupEventType != POPEVENT_PREVENTUNUSEDTILES && pEditor->m_PopupEventType != POPEVENT_IMAGEDIV16 && pEditor->m_PopupEventType != POPEVENT_IMAGE_MAX && pEditor->m_PopupEventType != POPEVENT_PIXELART_TOO_MANY_COLORS) { static int s_CancelButton = 0; if(pEditor->DoButton_Editor(&s_CancelButton, "Cancel", 0, &Button, 0, nullptr)) { pEditor->m_PopupEventWasActivated = false; + + if(pEditor->m_PopupEventType == POPEVENT_PIXELART_BIG_IMAGE || pEditor->m_PopupEventType == POPEVENT_PIXELART_MANY_COLORS) + { + free(pEditor->m_TileartImageInfo.m_pData); + pEditor->m_TileartImageInfo.m_pData = nullptr; + } + return CUI::POPUP_CLOSE_CURRENT; } } @@ -1831,6 +1862,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, { pEditor->PlaceBorderTiles(); } + else if(pEditor->m_PopupEventType == POPEVENT_PIXELART_BIG_IMAGE) + { + pEditor->TileartCheckColors(); + } + else if(pEditor->m_PopupEventType == POPEVENT_PIXELART_MANY_COLORS) + { + pEditor->AddTileart(); + } pEditor->m_PopupEventWasActivated = false; return CUI::POPUP_CLOSE_CURRENT; } diff --git a/src/game/editor/tileart.cpp b/src/game/editor/tileart.cpp new file mode 100644 index 000000000..0f33c8f50 --- /dev/null +++ b/src/game/editor/tileart.cpp @@ -0,0 +1,264 @@ +#include "editor.h" + +#include + +bool operator<(const ColorRGBA &Left, const ColorRGBA &Right) +{ + if(Left.r != Right.r) + return Left.r < Right.r; + else if(Left.g != Right.g) + return Left.g < Right.g; + else if(Left.b != Right.b) + return Left.b < Right.b; + else + return Left.a < Right.a; +} + +static int GetNumColorChannels(const CImageInfo &Image) +{ + switch(Image.m_Format) + { + case CImageInfo::FORMAT_RGB: + return 3; + case CImageInfo::FORMAT_SINGLE_COMPONENT: + return 1; + default: + return 4; + } +} + +static ColorRGBA GetPixelColor(const CImageInfo &Image, int x, int y) +{ + uint8_t *pData = static_cast(Image.m_pData); + int NumColorChannels = GetNumColorChannels(Image); + int PixelStartIndex = x * NumColorChannels + (Image.m_Width * NumColorChannels * y); + + ColorRGBA Color = {255, 255, 255, 255}; + if(NumColorChannels == 1) + { + Color.a = pData[PixelStartIndex]; + } + else + { + Color.r = pData[PixelStartIndex + 0]; + Color.g = pData[PixelStartIndex + 1]; + Color.b = pData[PixelStartIndex + 2]; + + if(NumColorChannels == 4) + Color.a = pData[PixelStartIndex + 3]; + } + + return Color; +} + +static void SetPixelColor(CImageInfo *pImage, int x, int y, ColorRGBA Color) +{ + uint8_t *pData = static_cast(pImage->m_pData); + int NumColorChannels = GetNumColorChannels(*pImage); + int PixelStartIndex = x * NumColorChannels + (pImage->m_Width * NumColorChannels * y); + + if(NumColorChannels == 1) + { + pData[PixelStartIndex] = Color.a; + } + else + { + pData[PixelStartIndex + 0] = Color.r; + pData[PixelStartIndex + 1] = Color.g; + pData[PixelStartIndex + 2] = Color.b; + + if(NumColorChannels == 4) + pData[PixelStartIndex + 3] = Color.a; + } +} + +static std::vector GetUniqueColors(const CImageInfo &Image) +{ + std::set ColorSet; + std::vector vUniqueColors; + for(int x = 0; x < Image.m_Width; x++) + { + for(int y = 0; y < Image.m_Height; y++) + { + ColorRGBA Color = GetPixelColor(Image, x, y); + if(Color.a > 0 && ColorSet.insert(Color).second) + vUniqueColors.push_back(Color); + } + } + std::sort(vUniqueColors.begin(), vUniqueColors.end()); + + return vUniqueColors; +} + +constexpr int NumTilesRow = 16; +constexpr int NumTilesColumn = 16; +constexpr int NumTiles = NumTilesRow * NumTilesColumn; +constexpr int TileSize = 64; + +static int GetColorIndex(const std::array &ColorGroup, ColorRGBA Color) +{ + std::array::const_iterator Iterator = std::find(ColorGroup.begin(), ColorGroup.end(), Color); + if(Iterator == ColorGroup.end()) + return 0; + return Iterator - ColorGroup.begin(); +} + +static std::vector> GroupColors(const std::vector &vColors) +{ + std::vector> vaColorGroups; + + for(size_t i = 0; i < vColors.size(); i += NumTiles - 1) + { + auto &Group = vaColorGroups.emplace_back(); + std::copy_n(vColors.begin() + i, std::min(NumTiles - 1, vColors.size() - i), Group.begin() + 1); + } + + return vaColorGroups; +} + +static void SetColorTile(CImageInfo *pImage, int x, int y, ColorRGBA Color) +{ + for(int i = 0; i < TileSize; i++) + { + for(int j = 0; j < TileSize; j++) + SetPixelColor(pImage, x * TileSize + i, y * TileSize + j, Color); + } +} + +static CImageInfo ColorGroupToImage(const std::array &aColorGroup) +{ + CImageInfo Image; + Image.m_Width = NumTilesRow * TileSize; + Image.m_Height = NumTilesColumn * TileSize; + Image.m_Format = CImageInfo::FORMAT_RGBA; + + uint8_t *pData = static_cast(malloc(static_cast(Image.m_Width) * Image.m_Height * 4 * sizeof(uint8_t))); + Image.m_pData = pData; + + for(int y = 0; y < NumTilesColumn; y++) + { + for(int x = 0; x < NumTilesRow; x++) + { + int ColorIndex = x + NumTilesRow * y; + SetColorTile(&Image, x, y, aColorGroup[ColorIndex]); + } + } + + return Image; +} + +static std::vector ColorGroupsToImages(const std::vector> &vaColorGroups) +{ + std::vector vImages; + vImages.reserve(vaColorGroups.size()); + for(const auto &ColorGroup : vaColorGroups) + vImages.push_back(ColorGroupToImage(ColorGroup)); + + return vImages; +} + +static std::shared_ptr ImageInfoToEditorImage(CEditor *pEditor, const CImageInfo &Image, const char *pName) +{ + std::shared_ptr pEditorImage = std::make_shared(pEditor); + pEditorImage->m_Width = Image.m_Width; + pEditorImage->m_Height = Image.m_Height; + pEditorImage->m_Format = Image.m_Format; + pEditorImage->m_pData = Image.m_pData; + + int TextureLoadFlag = pEditor->Graphics()->HasTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; + pEditorImage->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image.m_Width, Image.m_Height, Image.m_Format, Image.m_pData, CImageInfo::FORMAT_AUTO, TextureLoadFlag, pName); + pEditorImage->m_External = 0; + str_copy(pEditorImage->m_aName, pName); + + return pEditorImage; +} + +static std::shared_ptr AddLayerWithImage(CEditor *pEditor, const std::shared_ptr &pGroup, int Width, int Height, const CImageInfo &Image, const char *pName) +{ + std::shared_ptr pEditorImage = ImageInfoToEditorImage(pEditor, Image, pName); + pEditor->m_Map.m_vpImages.push_back(pEditorImage); + + std::shared_ptr pLayer = std::make_shared(Width, Height); + str_copy(pLayer->m_aName, pName); + pLayer->m_pEditor = pEditor; + pLayer->m_Image = pEditor->m_Map.m_vpImages.size() - 1; + pGroup->AddLayer(pLayer); + + return pLayer; +} + +static void SetTilelayerIndices(const std::shared_ptr &pLayer, const std::array &aColorGroup, const CImageInfo &Image) +{ + for(int x = 0; x < pLayer->m_Width; x++) + { + for(int y = 0; y < pLayer->m_Height; y++) + pLayer->m_pTiles[x + y * pLayer->m_Width].m_Index = GetColorIndex(aColorGroup, GetPixelColor(Image, x, y)); + } +} + +void CEditor::AddTileart() +{ + std::shared_ptr pGroup = m_Map.NewGroup(); + str_copy(pGroup->m_aName, m_aTileartFilename); + + auto vUniqueColors = GetUniqueColors(m_TileartImageInfo); + auto vaColorGroups = GroupColors(vUniqueColors); + auto vColorImages = ColorGroupsToImages(vaColorGroups); + 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); + 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(); + + free(m_TileartImageInfo.m_pData); + m_TileartImageInfo.m_pData = nullptr; + m_Map.OnModify(); + m_Dialog = DIALOG_NONE; +} + +void CEditor::TileartCheckColors() +{ + auto vUniqueColors = GetUniqueColors(m_TileartImageInfo); + int NumColorGroups = std::ceil(vUniqueColors.size() / 255.0f); + if(m_Map.m_vpImages.size() + NumColorGroups >= 64) + { + m_PopupEventType = CEditor::POPEVENT_PIXELART_TOO_MANY_COLORS; + m_PopupEventActivated = true; + free(m_TileartImageInfo.m_pData); + m_TileartImageInfo.m_pData = nullptr; + } + else if(NumColorGroups > 1) + { + m_PopupEventType = CEditor::POPEVENT_PIXELART_MANY_COLORS; + m_PopupEventActivated = true; + } + else + AddTileart(); +} + +bool CEditor::CallbackAddTileart(const char *pFilepath, int StorageType, void *pUser) +{ + CEditor *pEditor = (CEditor *)pUser; + + if(!pEditor->Graphics()->LoadPNG(&pEditor->m_TileartImageInfo, pFilepath, StorageType)) + { + pEditor->ShowFileDialogError("Failed to load image from file '%s'.", pFilepath); + return false; + } + + IStorage::StripPathAndExtension(pFilepath, pEditor->m_aTileartFilename, sizeof(pEditor->m_aTileartFilename)); + if(pEditor->m_TileartImageInfo.m_Width * pEditor->m_TileartImageInfo.m_Height > 10'000) + { + pEditor->m_PopupEventType = CEditor::POPEVENT_PIXELART_BIG_IMAGE; + pEditor->m_PopupEventActivated = true; + return false; + } + else + { + pEditor->TileartCheckColors(); + return false; + } +}