diff --git a/.gitignore b/.gitignore index 78bbd56..f6ef77c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,14 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# miyehn +.idea/ +cmake-build-debug/ +cmake-build-release/ +*.psd +*.index +*.islandblueprint + # User-specific files *.rsuser *.suo @@ -430,4 +438,4 @@ iOSInjectionProject/ # Scons *.o -.sconsign.dblite \ No newline at end of file +.sconsign.dblite diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b69f3a..0662fc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,5 +9,8 @@ project(Psd) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -O3") +add_definitions(-DPROJECT_DIR="${CMAKE_SOURCE_DIR}") + add_subdirectory(src/Psd) add_subdirectory(src/Samples) +add_subdirectory(src/Echoes) diff --git a/README.md b/README.md index e55c276..8020ff6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ +# Echoes Exporter + +This is the hub for some of Echoes' art pipeline tools. Below are the 3 relevant build targets. + +To learn more about the game Echoes and its art pipeline, see [this documentation page](https://miyehn.me/#page/echoes). + +### EchoesExporter_Windowed + +![](assets/psd-exporter.png) + +A GUI tool that exports props and their position & pivot information directly from PSD layers, to help streamline the 2D art asset workflow from PSD files to Unity prefabs. Unlike other tools, this one has GUI because it was mostly used by artists, who aren't necessarily familiar with the command line. See the "Echoes exporter" section of documentation for details. + +[Here](assets/EchoesExporter_Windowed.exe) is the executable. You can run it on [this sample PSD file](assets/sample-psd.psd), or make your own by following [this documentation I wrote](https://docs.google.com/document/d/1PNKSJYEmk9Cz_OFGKrXTKq-tnm2wgDN1cy6o5KLHpAY/edit?usp=sharing). + +### EchoesGatherSprites + +A command-line script that parses the game project directory and exports a PSD file containing all our 2D props, so they can be reused in environment set-dressing. We mostly do set-dressing in PSD files, not Unity. See the "Island blueprints" section of documentation for details. + +It also exports an index file of all props, which is then used by EchoesMakeBlueprint. + +![](assets/gather-sprites.png) + +### EchoesMakeBlueprint + +A command-line tool that takes the index file and turns the set-dressed scene (a PSD file) into a JSON describing the scene, so Unity can read from it and assemble the exact same scene in the game engine. + +![](assets/make-blueprint.png) + + +--- + # psd_sdk A C++ library that directly reads Photoshop PSD files. The library supports: * Groups diff --git a/assets/EchoesExporter_Windowed.exe b/assets/EchoesExporter_Windowed.exe new file mode 100644 index 0000000..05ac058 Binary files /dev/null and b/assets/EchoesExporter_Windowed.exe differ diff --git a/assets/exporter.png b/assets/exporter.png new file mode 100644 index 0000000..ce09605 Binary files /dev/null and b/assets/exporter.png differ diff --git a/assets/gather-sprites.png b/assets/gather-sprites.png new file mode 100644 index 0000000..c0633fe Binary files /dev/null and b/assets/gather-sprites.png differ diff --git a/assets/make-blueprint.png b/assets/make-blueprint.png new file mode 100644 index 0000000..a31847e Binary files /dev/null and b/assets/make-blueprint.png differ diff --git a/assets/psd-exporter.png b/assets/psd-exporter.png new file mode 100644 index 0000000..e9ec4c8 Binary files /dev/null and b/assets/psd-exporter.png differ diff --git a/src/Echoes/AssetPack.cpp b/src/Echoes/AssetPack.cpp new file mode 100644 index 0000000..e2ed481 --- /dev/null +++ b/src/Echoes/AssetPack.cpp @@ -0,0 +1,710 @@ +#include +#include +#include +#include "AssetPack.h" +#include "Log.h" + +// import +#include "../Psd/Psd.h" +#include "../Psd/PsdMallocAllocator.h" +#include "../Psd/PsdLayerMaskSection.h" +#include "../Psd/PsdNativeFile.h" +#include "../Psd/PsdDocument.h" +#include "../Psd/PsdParseDocument.h" +#include "../Psd/PsdColorMode.h" +#include "../Psd/PsdLayer.h" +#include "../Psd/PsdParseLayerMaskSection.h" +#include "../Psd/PsdLayerCanvasCopy.h" +#include "../Psd/PsdChannel.h" +#include "../Psd/PsdInterleave.h" +#include "../Psd/PsdChannelType.h" +#include "../Psd/PsdLayerType.h" +#include "../Psd/PsdBlendMode.h" + +// export +#include "Utils.h" +#include "stb_image_resize.h" +#include "stb_image_write.h" +#include "json/json.h" + +// helpers for reading PSDs +namespace +{ +PSD_USING_NAMESPACE; +static const unsigned int CHANNEL_NOT_FOUND = UINT_MAX; + + +// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- +template +static void* ExpandChannelToCanvas(Allocator* allocator, const DataHolder* layer, const void* data, unsigned int canvasWidth, unsigned int canvasHeight) +{ + T* canvasData = static_cast(allocator->Allocate(sizeof(T)*canvasWidth*canvasHeight, 16u)); + memset(canvasData, 0u, sizeof(T)*canvasWidth*canvasHeight); + + imageUtil::CopyLayerData(static_cast(data), canvasData, layer->left, layer->top, layer->right, layer->bottom, canvasWidth, canvasHeight); + + return canvasData; +} + + +// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- +static void* ExpandChannelToCanvas(const Document* document, Allocator* allocator, Layer* layer, Channel* channel) +{ + if (document->bitsPerChannel == 8) + return ExpandChannelToCanvas(allocator, layer, channel->data, document->width, document->height); + else if (document->bitsPerChannel == 16) + return ExpandChannelToCanvas(allocator, layer, channel->data, document->width, document->height); + else if (document->bitsPerChannel == 32) + return ExpandChannelToCanvas(allocator, layer, channel->data, document->width, document->height); + + return nullptr; +} + + +// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- +template +static void* ExpandMaskToCanvas(const Document* document, Allocator* allocator, T* mask) +{ + if (document->bitsPerChannel == 8) + return ExpandChannelToCanvas(allocator, mask, mask->data, document->width, document->height); + else if (document->bitsPerChannel == 16) + return ExpandChannelToCanvas(allocator, mask, mask->data, document->width, document->height); + else if (document->bitsPerChannel == 32) + return ExpandChannelToCanvas(allocator, mask, mask->data, document->width, document->height); + + return nullptr; +} + + +// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- +unsigned int FindChannel(Layer* layer, int16_t channelType) +{ + for (unsigned int i = 0; i < layer->channelCount; ++i) + { + Channel* channel = &layer->channels[i]; + if (channel->data && channel->type == channelType) + return i; + } + + return CHANNEL_NOT_FOUND; +} + + +// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- +template +T* CreateInterleavedImage(Allocator* allocator, const void* srcR, const void* srcG, const void* srcB, unsigned int width, unsigned int height) +{ + T* image = static_cast(allocator->Allocate(width*height * 4u*sizeof(T), 16u)); + + const T* r = static_cast(srcR); + const T* g = static_cast(srcG); + const T* b = static_cast(srcB); + imageUtil::InterleaveRGB(r, g, b, T(0), image, width, height); + + return image; +} + + +// --------------------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------------------- +template +T* CreateInterleavedImage(Allocator* allocator, const void* srcR, const void* srcG, const void* srcB, const void* srcA, unsigned int width, unsigned int height) +{ + T* image = static_cast(allocator->Allocate(width*height * 4u*sizeof(T), 16u)); + + const T* r = static_cast(srcR); + const T* g = static_cast(srcG); + const T* b = static_cast(srcB); + const T* a = static_cast(srcA); + imageUtil::InterleaveRGBA(r, g, b, a, image, width, height); + + return image; +} + +// a few more helpers + +enum PositionParseStatus { + NotParsed, + ParsedSizeOnly, + ParseDone, +}; + +uint8_t* GetLayerData(const Document* document, File* file, Allocator &allocator, Layer* layer) { + const unsigned int indexR = FindChannel(layer, channelType::R); + const unsigned int indexG = FindChannel(layer, channelType::G); + const unsigned int indexB = FindChannel(layer, channelType::B); + const unsigned int indexA = FindChannel(layer, channelType::TRANSPARENCY_MASK); + const bool allChannelsFound = + indexR!=CHANNEL_NOT_FOUND && + indexG!=CHANNEL_NOT_FOUND && + indexB!=CHANNEL_NOT_FOUND && + indexA!=CHANNEL_NOT_FOUND; + + if (!allChannelsFound) { + ERR("some layer channels were not found!") + return nullptr; + } + + void* canvasData[4] = { + ExpandChannelToCanvas(document, &allocator, layer, &layer->channels[indexR]), + ExpandChannelToCanvas(document, &allocator, layer, &layer->channels[indexG]), + ExpandChannelToCanvas(document, &allocator, layer, &layer->channels[indexB]), + ExpandChannelToCanvas(document, &allocator, layer, &layer->channels[indexA]), + }; + uint8_t* layerData = CreateInterleavedImage( + &allocator, canvasData[0], canvasData[1], canvasData[2], canvasData[3], document->width, document->height); + allocator.Free(canvasData[0]); + allocator.Free(canvasData[1]); + allocator.Free(canvasData[2]); + allocator.Free(canvasData[3]); + return layerData; +} +} + +bool AssetPack::isValid() const { + bool valid = true; + for (auto& spritePair : spriteSets) { + auto& sprite = spritePair.second; + if (sprite.baseLayersData.empty()) { + std::string baseName = sprite.getBaseName(); + if (baseName.empty()) baseName = "(unknown)"; + AppendToGUILog({LT_ERROR, "ERROR: sprite '" + baseName + "' doesn't have a base layer!"}); + valid = false; + } + } + return valid; +} + +bool EchoesReadPsdToAssetPack(const std::string& inFile, AssetPack& assetPack) { + + psd::Document* document; + psd::LayerMaskSection* section; + psd::MallocAllocator allocator; + psd::NativeFile file(&allocator); + if (!ReadPsdLayers(inFile, document, section, allocator, file)) { + return false; + } + + //////////////////////////////////////////////////////////////// + // start building the asset pack + + // metadata + assetPack.docWidth = document->width; + assetPack.docHeight = document->height; + + // layers first pass + for (int i = 0; i < section->layerCount; i++) { + psd::Layer* layer = §ion->layers[i]; + // info layers: + if (layer->parent != nullptr && tolower(GetName(layer->parent))=="meta") { + if (tolower(GetName(layer)) == "origin") { + assetPack.docOriginPx = { + (layer->right + layer->left) * 0.5f, + (layer->bottom + layer->top) * 0.5f + }; + } else { + auto tokens = SplitTokens(GetName(layer)); + if (tokens.size() == 2 && tolower(tokens[0]) == "ruler") { + assetPack.pixelsPerDiagonalUnit = (layer->right - layer->left) / std::stof(tokens[1]); + } + } + } + // export layers: just create something empty and insert into dict for now + else if ( + VisibleInHierarchy(layer) && + (layer->type == layerType::OPEN_FOLDER || layer->type == layerType::CLOSED_FOLDER) && + layer->parent != nullptr && + tolower(GetName(layer->parent))=="export") + { + auto name = GetName(layer); + assetPack.spriteSets[name] = SpriteSet(); + if (layer->layerMask) { + WARN("folder for sprite '%s' has a layer mask, which will be ignored during export..", name.c_str()) + AppendToGUILog({LT_WARNING, "folder for sprite '" + name + "' has a layer mask, which will be ignored during export.."}); + } + } + } + + // layers second pass: actually parse data + uint32_t layerDataSize = document->width * document->height * 4; + + const Layer* currentSpriteDivider = nullptr; + const Layer* currentSpriteFolder = nullptr; + const Layer* prevLayer = nullptr; + SpriteSet* currentSprite = nullptr; + std::string currentSpriteName; + PositionParseStatus positionParseStatus = ParseDone; + auto ProcessSpriteMetaData = [&](Layer* layer) { + if (layer->layerMask) { + WARN("base layer(s) of '%s' has a layer mask, which will be ignored during export..", currentSpriteName.c_str()) + AppendToGUILog({LT_WARNING, "base layer(s) of '" + currentSpriteName + "' has a layer mask, which will be ignored during export.."}); + } + if (blendMode::KeyToEnum(layer->blendModeKey) != blendMode::NORMAL) { + WARN("base layer of '%s' doesn't have normal blend mode- result might look different.", currentSpriteName.c_str()) + AppendToGUILog({LT_WARNING, "base layer of '" + currentSpriteName + "' doesn't have normal blend mode- result might look different."}); + } + // name + currentSprite->name = currentSpriteName; + + // unit dims + auto tokens = SplitTokens(GetName(layer)); + try { + if (tokens.size() == 4) { + currentSprite->minUnit = {std::stof(tokens[0]), std::stof(tokens[1])}; + currentSprite->sizeUnit = {std::stof(tokens[2]), std::stof(tokens[3])}; + positionParseStatus = ParseDone; + } else if (tokens.size() == 2) { + currentSprite->sizeUnit = {std::stof(tokens[0]), std::stof(tokens[1])}; + positionParseStatus = ParsedSizeOnly; + } + return true; + } catch(std::exception &e) { + std::string msg = "Failed to parse position and size information for sprite '" + currentSprite->name + "'"; + AppendToGUILog({LT_WARNING, msg + ". Did you name the base layer with 2 or 4 numbers?"}); + WARN("%s: %s", msg.c_str(), e.what()) + return false; + } + }; + auto ExpandPixelBBox = [&](Layer* layer) { + // pixel dims + int minX = std::max(0, layer->left); + int minY = std::max(0, layer->top); + int maxX = std::min((int)document->width, layer->right); + int maxY = std::min((int)document->height, layer->bottom); + ivec2 minPx = { + std::min(minX, currentSprite->minPx.x), + std::min(minY, currentSprite->minPx.y) + }; + ivec2 maxPx = { + std::max(maxX, currentSprite->minPx.x+currentSprite->sizePx.x), + std::max(maxY, currentSprite->minPx.y+currentSprite->sizePx.y) + }; + currentSprite->minPx = minPx; + currentSprite->sizePx = { maxPx.x - minPx.x, maxPx.y - minPx.y }; + }; + auto ProcessSpriteBaseOrLightLayerContent = [&](std::vector>& dst, Layer* layer) { + ASSERT(layer->type == layerType::ANY) + uint8_t* layerData = GetLayerData(document, &file, allocator, layer); + if (!layerData) { + file.Close(); return false; + } + dst.emplace_back(); + dst.back().resize(layerDataSize); + memcpy(dst.back().data(), layerData, layerDataSize); + allocator.Free(layerData); + return true; + }; + auto ProcessSpriteEmissionMaskContent = [&](std::vector& dst, Layer* layer) { + ASSERT(layer->type == layerType::ANY) + uint8_t* layerData = GetLayerData(document, &file, allocator, layer); + if (!layerData) { + file.Close(); return false; + } + uint32_t numPx = document->width * document->height; + // we only want the alpha channel + dst.resize(numPx); + for (uint32_t i = 0; i < numPx; i++) { + dst[i] = layerData[i * 4 + 3]; + } + allocator.Free(layerData); + return true; + }; + for (int i = 0; i < section->layerCount; i++) { + Layer* layer = §ion->layers[i]; + if (!IsOrUnderLayer(layer, "export") || (layer->type != layerType::SECTION_DIVIDER && !VisibleInHierarchy(layer))) continue; + + ExtractLayer(document, &file, &allocator, layer); + + // entering new sprite.. + if (layer->type == layerType::SECTION_DIVIDER && + layer->parent && // asset group + layer->parent->parent && tolower(GetName(layer->parent->parent)) == "export" && // "export" + assetPack.spriteSets.find(GetName(layer->parent)) != assetPack.spriteSets.end() // check there is a sprite set + ) { + // check if last sprite has position info fully parsed, give warning if not: + if (positionParseStatus != ParseDone) { + if (positionParseStatus == NotParsed) { + WARN("failed to parse position and size information for sprite %s; its pivot will be incorrect in Unity", currentSprite->name.c_str()) + AppendToGUILog({LT_WARNING, "failed to parse position and size for sprite '" + currentSprite->name + "'; its pivot will be incorrect in Unity. Refer to documentation for how to specify position and size information for sprites"}); + } else if (positionParseStatus == ParsedSizeOnly) { + WARN("didn't find position information for sprite %s, its pivot will be incorrect in Unity: did you forget to include the 'corner' layer?", currentSprite->name.c_str()) + AppendToGUILog({LT_WARNING, "didn't find position information for sprite '" + currentSprite->name + "', its pivot will be incorrect in Unity: did you forget to include the 'corner' layer?"}); + } + currentSprite->minUnit = {0, 0}; + currentSprite->sizeUnit = {1, 1}; + } + // actually move on to new layer: + currentSpriteDivider = layer; + currentSpriteFolder = layer->parent; + currentSpriteName = GetName(layer->parent); + currentSprite = &assetPack.spriteSets[GetName(layer->parent)]; + positionParseStatus = NotParsed; + // initial bbox, to be expanded.. + currentSprite->minPx = { (int)document->width, (int)document->height }; + currentSprite->sizePx = { -(int)document->width, -(int)document->height }; + } + + // direct child of sprite folder, but folder --> base container + else if ( + layer->parent == currentSpriteFolder && + (layer->type == layerType::CLOSED_FOLDER || layer->type == layerType::OPEN_FOLDER) + ) { + if (!ProcessSpriteMetaData(layer)) return false; + } + + // grand child of sprite folder, raster layer --> base content, maybe more than 1 + else if ( + layer->parent && layer->parent->parent == currentSpriteFolder && + layer->type == layerType::ANY + ) { + if (!ProcessSpriteBaseOrLightLayerContent(currentSprite->baseLayersData, layer)) return false; + ExpandPixelBBox(layer); + } + + // direct child of sprite folder, raster layer --> single base layer, or emission, or lightTex, or corner + else if ( + layer->parent == currentSpriteFolder && layer->type == layerType::ANY) { + // single base layer + if (prevLayer == currentSpriteDivider) { + if (!ProcessSpriteMetaData(layer)) { + file.Close(); + return false; + } + if (!ProcessSpriteBaseOrLightLayerContent(currentSprite->baseLayersData, layer)) { + file.Close(); + return false; + } + ExpandPixelBBox(layer); + } + // corner marker + else if (tolower(GetName(layer)) == "corner") { + if (positionParseStatus == ParsedSizeOnly) { + vec2 cornerPosPx = { + (layer->right + layer->left) * 0.5f, + (layer->bottom + layer->top) * 0.5f + }; + cornerPosPx = cornerPosPx - assetPack.docOriginPx; + currentSprite->minUnit = PixelPosToUnitPos(cornerPosPx, assetPack.pixelsPerDiagonalUnit); + + positionParseStatus = ParseDone; + } + } + // emission + else if (tolower(GetName(layer)) == "emission") { + if (!ProcessSpriteEmissionMaskContent(currentSprite->emissionMaskData, layer)) { + return false; + } + } + // light tex + else { + if (ProcessSpriteBaseOrLightLayerContent(currentSprite->lightLayersData, layer)) { + currentSprite->lightLayerNames.emplace_back(GetName(layer)); + } else { + return false; + } + } + } + prevLayer = layer; + } + + file.Close(); + return assetPack.isValid(); +} + + +/////////////////////////////////////////////////////////////////////////// + +#define EXPORT_PPU 100.0f + +// returns if write is successful +bool WritePngToDirectory( + const std::vector& data, + uint32_t width, uint32_t height, uint32_t numChannels, + const std::string& outDir, + const std::string& fileName, + float resizeRatio = 1.0f +) { + std::string fullpath = outDir + "/" + fileName; + + // resize + uint32_t newWidth = std::ceil(width * resizeRatio); + uint32_t newHeight = std::ceil(height * resizeRatio); + std::vector resizedData(newWidth * newHeight * numChannels); + stbir_resize_uint8( + data.data(), width, height, width * numChannels, + resizedData.data(), newWidth, newHeight, newWidth * numChannels, numChannels); + + // write + bool success = stbi_write_png(fullpath.c_str(), newWidth, newHeight, numChannels, resizedData.data(), numChannels * newWidth) != 0; + ASSERT(success) + return success; +} + +std::vector Crop(const std::vector& data, uint32_t srcStrideInBytes, uint32_t numChannels, ivec2 minPx, ivec2 sizePx) { + std::vector result(sizePx.x * sizePx.y * numChannels); + uint32_t dstStrideInBytes = sizePx.x * numChannels; + for (int i = 0; i < sizePx.y; i++) { + uint32_t srcStart = (minPx.y + i) * srcStrideInBytes + minPx.x * numChannels; + uint32_t dstStart = i * dstStrideInBytes; + memcpy(result.data() + dstStart, data.data() + srcStart, dstStrideInBytes); + } + return result; +} + +struct PivotInfo { + std::string texPath; + vec2 pivot; + Json::Value serialized() const { + Json::Value result; + result["texPath"] = texPath; + result["pivot"] = pivot.serialized(); + return result; + } +}; + +struct MaterialInfo { + std::string name; + std::string mainTexPath; + std::string emissionTexPath; + std::string light0TexPath; + std::string light0Message = "(none)"; + std::string light1TexPath; + std::string light1Message = "(none)"; + std::string light2TexPath; + std::string light2Message = "(none)"; + std::string light3TexPath; + std::string light3Message = "(none)"; + vec2 basePosition; + vec2 size; + Json::Value serialized() const { + Json::Value result; + result["name"] = name; + result["mainTexPath"] = mainTexPath; + result["emissionTexPath"] = emissionTexPath; + result["light0TexPath"] = light0TexPath; + result["light0Message"] = light0Message; + result["light1TexPath"] = light1TexPath; + result["light1Message"] = light1Message; + result["light2TexPath"] = light2TexPath; + result["light2Message"] = light2Message; + result["light3TexPath"] = light3TexPath; + result["light3Message"] = light3Message; + result["basePosition"] = basePosition.serialized(); + result["size"] = size.serialized(); + return result; + } +}; + +std::string SpriteSet::getBaseName() const { + return JoinTokens(SplitTokens(name)); +} + +std::string SpriteSet::getBaseTexPath(int index) const { + return getBaseName() + "_base"+std::to_string(index)+".png"; +} + +std::string SpriteSet::getLayoutPngPath(const std::string& assetPackName, int index) const { + std::string result = assetPackName + "&" + getBaseName(); + if (baseLayersData.size() > 1) { + result += "_part" + std::to_string(index); + } + return result + "#.png"; +} + +std::string SpriteSet::getLightTexPath(int index) const { + //std::string baseName = JoinTokens(SplitTokens(name)); + return getBaseName() + "_L" + std::to_string(index) +".png"; +} + +std::string SpriteSet::getEmissionTexPath() const { + return getBaseName() + "_emission.png"; +} + +float ComputeResizeRatio(float pixelsPerDiagonalUnit) { + const float standardPPDU = std::sqrt(2.0f) * EXPORT_PPU; + const float resizeRatio = standardPPDU / pixelsPerDiagonalUnit; + return resizeRatio; +} +// a list of all base textures to their anchor points + +// also a list of materials, each: +// base tex + all the light textures +// base name + +std::string SerializeAssetPack(const AssetPack& assetPack) { + Json::Value root; + + // pivots (only base layers need them; others are just used as shader textures) + Json::Value pivots; + int pivotIdx = 0; + for (auto& spritePair : assetPack.spriteSets) { + auto& sprite = spritePair.second; + vec2 anchorUnit = sprite.minUnit + sprite.sizeUnit / 2; + vec2 relAnchorPx = UnitPosToPixelPos(anchorUnit, assetPack.pixelsPerDiagonalUnit); + vec2 anchorPx = relAnchorPx + assetPack.docOriginPx - sprite.minPx; + vec2 anchorNormalized = { + anchorPx.x / sprite.sizePx.x, + 1.0f - anchorPx.y / sprite.sizePx.y // since unity wants this reversed + }; + for (int baseLayerIdx = 0; baseLayerIdx < sprite.baseLayersData.size(); baseLayerIdx++) { + PivotInfo pivot = { + sprite.getBaseTexPath(baseLayerIdx), + anchorNormalized + }; + pivots[pivotIdx] = pivot.serialized(); + LOG("%s pivot at (%.3f, %.3f)", sprite.getBaseTexPath(baseLayerIdx).c_str(), pivot.pivot.x, pivot.pivot.y) + pivotIdx++; + } + } + root["pivots"] = pivots; + + // materials + Json::Value materials; + int matIdx = 0; + for (auto& spritePair : assetPack.spriteSets) { + auto &sprite = spritePair.second; + for (int baseLayerIdx = 0; baseLayerIdx < sprite.baseLayersData.size(); baseLayerIdx++) { + MaterialInfo mat; + mat.name = sprite.getBaseName(); + if (sprite.baseLayersData.size() > 1) { + mat.name += "_part" + std::to_string(baseLayerIdx); + } + LOG("exporting material '%s'..", mat.name.c_str()) + mat.mainTexPath = sprite.getBaseTexPath(baseLayerIdx); + mat.basePosition = sprite.minUnit; + mat.size = sprite.sizeUnit; + for (int lightLayerIdx = 0; lightLayerIdx < sprite.lightLayersData.size(); lightLayerIdx++) { + if (lightLayerIdx == 0) { + mat.light0TexPath = sprite.getLightTexPath(0); + mat.light0Message = sprite.lightLayerNames[0]; + } + else if (lightLayerIdx == 1) { + mat.light1TexPath = sprite.getLightTexPath(1); + mat.light1Message = sprite.lightLayerNames[1]; + } + else if (lightLayerIdx == 2) { + mat.light2TexPath = sprite.getLightTexPath(2); + mat.light2Message = sprite.lightLayerNames[2]; + } + else if (lightLayerIdx == 3) { + mat.light3TexPath = sprite.getLightTexPath(3); + mat.light3Message = sprite.lightLayerNames[3]; + } + else ASSERT(false) + } + // emission + if (!sprite.emissionMaskData.empty()) { + mat.emissionTexPath = sprite.getEmissionTexPath(); + } + + materials[matIdx] = mat.serialized(); + matIdx++; + } + } + root["materials"] = materials; + + std::stringstream stream; + stream << root; + return stream.str(); +} + +bool ExportAssetPack(const AssetPack& assetPack, const std::string& destination, const std::string& folderName, int cleanFirst, ExportType exportType) { + + std::string outDir = destination + "\\" + folderName; + if (exportType == ET_LAYOUT) { + outDir += " (layout)"; + } + + // remove everything from previous export + if (cleanFirst > 0 && std::filesystem::exists(outDir)) { + std::filesystem::remove_all(outDir); + } + + // from: https://stackoverflow.com/questions/9235679/create-a-directory-if-it-doesnt-exist + std::filesystem::create_directories(outDir); + + // compute resize ratio + const float resizeRatio = ComputeResizeRatio(assetPack.pixelsPerDiagonalUnit); + LOG("resize ratio: %f", resizeRatio) + //LOG("resize ratio: %.3f", resizeRatio) + if (resizeRatio > 1) { + AppendToGUILog({LT_WARNING, "WARNING: resize ratio is " + std::to_string(resizeRatio) + " (>1)"}); + } + + const std::string writeFileError = "ERROR: cannot write file(s). If you are updating existing sprites, make sure to first check them out in perforce."; + + if (exportType == ET_ASSETPACK) { + for (auto& pair : assetPack.spriteSets) { + + const SpriteSet& sprite = pair.second; + for (int i = 0; i < sprite.baseLayersData.size(); i++) { + auto baseRegionData = Crop(sprite.baseLayersData[i], assetPack.docWidth * 4, 4, sprite.minPx, sprite.sizePx); + if (!WritePngToDirectory( + baseRegionData, sprite.sizePx.x, sprite.sizePx.y, 4, outDir, + sprite.getBaseTexPath(i), resizeRatio)) + { + AppendToGUILog({LT_ERROR, writeFileError}); + return false; + } + } + for (int i = 0; i < sprite.lightLayersData.size(); i++) { + auto lightTexRegionData = Crop(sprite.lightLayersData[i], assetPack.docWidth * 4, 4, sprite.minPx, + sprite.sizePx); + if (!WritePngToDirectory( + lightTexRegionData, sprite.sizePx.x, sprite.sizePx.y, 4, outDir, + sprite.getLightTexPath(i), resizeRatio)) + { + AppendToGUILog({LT_ERROR, writeFileError}); + return false; + } + } + // emission + if (!sprite.emissionMaskData.empty()) { + auto croppedEmissionMask = Crop(sprite.emissionMaskData, assetPack.docWidth, 1, sprite.minPx, sprite.sizePx); + if (!WritePngToDirectory( + croppedEmissionMask, sprite.sizePx.x, sprite.sizePx.y, 1, outDir, + sprite.getEmissionTexPath(), resizeRatio)) + { + AppendToGUILog({LT_ERROR, writeFileError}); + return false; + } + } + } + + // also export a json + std::string outstr = SerializeAssetPack(assetPack); + + // write to file + std::string folderName = outDir.substr(outDir.find_last_of("/\\") + 1); + if (WriteStringToFile(outDir + "/" + folderName + ".assetpack", outstr)) { + // write succeeded + } else { + AppendToGUILog({LT_ERROR, writeFileError}); + return false; + } + return true; + } + else if (exportType == ET_LAYOUT) { + for (auto& pair : assetPack.spriteSets) { + const SpriteSet& sprite = pair.second; + for (int i = 0; i < sprite.baseLayersData.size(); i++) { + auto baseRegionData = Crop(sprite.baseLayersData[i], assetPack.docWidth * 4, 4, sprite.minPx, sprite.sizePx); + if (!WritePngToDirectory( + baseRegionData, sprite.sizePx.x, sprite.sizePx.y, 4, outDir, + sprite.getLayoutPngPath(folderName, i), resizeRatio)) + { + AppendToGUILog({LT_ERROR, writeFileError}); + return false; + } + } + } + return true; + } + + return false; +} diff --git a/src/Echoes/AssetPack.h b/src/Echoes/AssetPack.h new file mode 100644 index 0000000..8350ca2 --- /dev/null +++ b/src/Echoes/AssetPack.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include "Utils.h" + +struct SpriteSet { + std::string name; + std::vector> baseLayersData; + std::vector> lightLayersData; + std::vector lightLayerNames; + std::vector emissionMaskData; + // rel to doc canvas, in pixels + ivec2 minPx; + ivec2 sizePx; + // rel to doc origin, in units (parsed from base layer name) + vec2 minUnit; + vec2 sizeUnit; + + std::string getBaseName() const; + std::string getBaseTexPath(int index) const; + std::string getLightTexPath(int index) const; + std::string getEmissionTexPath() const; + std::string getLayoutPngPath(const std::string& assetPackName, int index) const; +}; + +struct AssetPack { + std::unordered_map spriteSets; + vec2 docOriginPx; + uint32_t docWidth, docHeight; + float pixelsPerDiagonalUnit = STANDARD_PPDU; // sqrt(2) = 1.41421356237 + + bool isValid() const; +}; + +bool EchoesReadPsdToAssetPack(const std::string& inFile, AssetPack& assetPack); + +enum ExportType { + ET_ASSETPACK = 0, + ET_LAYOUT = 1 +}; + +bool ExportAssetPack(const AssetPack& assetPack, const std::string& destination, const std::string& folderName, int cleanFirst, ExportType exportType); + +///////////////// windows api specific: \ No newline at end of file diff --git a/src/Echoes/CMakeLists.txt b/src/Echoes/CMakeLists.txt new file mode 100644 index 0000000..497ab84 --- /dev/null +++ b/src/Echoes/CMakeLists.txt @@ -0,0 +1,49 @@ +# CMake build for psd_sdk samples +# Copyright 2023, heavenstone +# See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause) + +cmake_minimum_required(VERSION 3.2) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +project(Echoes) + +include_directories(../External) + +add_definitions(-DWINOS) + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG=1") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DDEBUG=0") + +set(echoes_exporter_source_win + WinMain.cpp + AssetPack.cpp + Utils.cpp + ../External/stb_image_write.cpp + ../External/stb_image_resize.cpp + ../External/jsoncpp.cpp +) + +set(echoes_gather_sprites_source + GatherSprites.cpp + Utils.cpp + ../External/stb_image.cpp + ../External/jsoncpp.cpp +) + +set(echoes_make_blueprint_source + MakeBlueprint.cpp + Utils.cpp + ../External/jsoncpp.cpp +) + +add_executable(${PROJECT_NAME}Exporter_Windowed WIN32 ${echoes_exporter_source_win}) +add_executable(${PROJECT_NAME}GatherSprites ${echoes_gather_sprites_source}) +add_executable(${PROJECT_NAME}MakeBlueprint ${echoes_make_blueprint_source}) + +target_compile_definitions(${PROJECT_NAME}Exporter_Windowed PRIVATE ECHOES_EXPORTER_WINDOWED=1) + +target_link_libraries(${PROJECT_NAME}Exporter_Windowed Psd) +target_link_libraries(${PROJECT_NAME}GatherSprites Psd) +target_link_libraries(${PROJECT_NAME}MakeBlueprint Psd) diff --git a/src/Echoes/GatherSprites.cpp b/src/Echoes/GatherSprites.cpp new file mode 100644 index 0000000..30f289b --- /dev/null +++ b/src/Echoes/GatherSprites.cpp @@ -0,0 +1,237 @@ +// +// Created by miyehn on 12/27/2023. +// +// import +#include +#include + +#include "../Psd/Psd.h" +#include "../Psd/PsdMallocAllocator.h" +#include "../Psd/PsdNativeFile.h" +#include "../Psd/PsdLayer.h" +#include "../Psd/PsdExport.h" +#include "../Psd/PsdExportDocument.h" + +#include "Log.h" +#include "Utils.h" +#include "AssetPack.h" + +#include "cxxopts.hpp" +#include "../External/stb_image.h" +#include "json/json.h" + +bool AddSprite(psd::ExportDocument* document, psd::Allocator &allocator, const std::string& spritePath, const std::string& pngPath, int left, int top, int maxSize=1024) { + //////////////////////////////////////////////////////////////// + // load png + int nativeChannels = 0; + int iWidth = 0; int iHeight = 0; + uint8_t* pixels = stbi_load(pngPath.c_str(), &iWidth, &iHeight, &nativeChannels, 4); + + if (iWidth > maxSize || iHeight > maxSize) { + WARN("Skipping sprite '%s' because it's too big (%ix%i)", spritePath.c_str(), iWidth, iHeight) + return false; + } + + uint32_t numPixels = iWidth * iHeight; + std::vector channelR, channelG, channelB, channelA; + channelR.resize(numPixels); + channelG.resize(numPixels); + channelB.resize(numPixels); + channelA.resize(numPixels); + for (int i = 0; i < numPixels; i++) { + channelR[i] = pixels[i * 4 + 0]; + channelG[i] = pixels[i * 4 + 1]; + channelB[i] = pixels[i * 4 + 2]; + channelA[i] = pixels[i * 4 + 3]; + } + + // add a separator here to end the sprite name (makes parsing easier) + const unsigned int layer1 = AddLayer(document, &allocator, (spritePath + "#").c_str()); + + UpdateLayer(document, &allocator, layer1, psd::exportChannel::RED, left, top, left + iWidth, top + iHeight, channelR.data(), psd::compressionType::RAW); + UpdateLayer(document, &allocator, layer1, psd::exportChannel::GREEN, left, top, left + iWidth, top + iHeight, channelG.data(), psd::compressionType::RAW); + UpdateLayer(document, &allocator, layer1, psd::exportChannel::BLUE, left, top, left + iWidth, top + iHeight, channelB.data(), psd::compressionType::RAW); + UpdateLayer(document, &allocator, layer1, psd::exportChannel::ALPHA, left, top, left + iWidth, top + iHeight, channelA.data(), psd::compressionType::RAW); + + LOG("loaded '%s' w=%d, h=%d", spritePath.c_str(), iWidth, iHeight) + return true; +} + +struct SpriteInfo { + std::string spritePath; + std::string pngPath; + Json::Value pivot; + Json::Value size; + bool includeInPsd; +}; + +bool EchoesGatherSprites(const std::vector& spritesToLoad, const std::string& outFilePath) { + const std::string outPsd = outFilePath + ".psd"; + const std::wstring fullPath(outPsd.c_str(), outPsd.c_str() + outPsd.length()); + psd::MallocAllocator allocator; + psd::NativeFile file(&allocator); + + // try opening the file. if it fails, bail out. + if (!file.OpenWrite(fullPath.c_str())) + { + ERR("failed to open file for write") + return false; + } + + psd::ExportDocument* document = CreateExportDocument(&allocator, 2048, 2048, 8u, psd::exportColorMode::RGB); + AddMetaData(document, &allocator, "author", "miyehn"); + + ///////////////////// + + Json::Value indexRoot; + int spriteIdx = 0; + for (const auto &sprite: spritesToLoad) { + + // add to psd + if (sprite.includeInPsd) { + AddSprite(document, allocator, sprite.spritePath, sprite.pngPath, 0, 0, 1400); + } + + // add to index (always) + Json::Value spriteInfoJson; + spriteInfoJson["spritePath"] = sprite.spritePath; + spriteInfoJson["pivot"] = sprite.pivot; + spriteInfoJson["size"] = sprite.size; + indexRoot[spriteIdx] = spriteInfoJson; + spriteIdx++; + } + + WriteDocument(document, &allocator, &file); + DestroyExportDocument(document, &allocator); + file.Close(); + LOG("done writing .psd") + + ///////////////////// + std::stringstream stream; + stream << indexRoot; + if (WriteStringToFile(outFilePath + ".index", stream.str())) { + LOG("done writing .index") + return true; + } + return false; +} + +int main(int argc, const char* argv[]) { + + cxxopts::Options options("EchoesGatherSprites", "Documentation: TODO\n"); + options.allow_unrecognised_options(); + +#if 1 + options.add_options() + ("d,directory", "directory to iterate, absolute path or relative to executable", cxxopts::value()/*->default_value("input.psd")*/) + ("o,output", "output name (psd & index), absolute path or relative to executable", cxxopts::value()/*->default_value("output")*/) + ("x,ignore", "sprite path ignore list (regex, one per line)", cxxopts::value()/*->default_value("output")*/) + ("i,include", "include list (regex, one per line)", cxxopts::value()/*->default_value("output")*/) + ("l,listonly", "list sprites only; don't create psd", cxxopts::value()) + ; + auto result = options.parse(argc, argv); + + std::string inDirectory, outPsd, ignoreListFile, includeListFile; + bool listOnly; + try { + inDirectory = result["directory"].as(); + outPsd = result["output"].as(); + ignoreListFile = result["ignore"].as(); + includeListFile = result["include"].as(); + listOnly = result["listonly"].as(); + LOG("dir: %s, out: %s, ignore: %s, include: %s", inDirectory.c_str(), outPsd.c_str(), ignoreListFile.c_str(), includeListFile.c_str()) + } catch (std::exception &e) { + std::cout << options.help() << std::endl; + return 0; + } + + std::vector ignoreList; + {// read from ignoreListFile to populate + std::vector list = SplitLines(ReadFileAsString(ignoreListFile)); + for (const auto &line: list) { + if (!line.empty() && line[0] != '#') ignoreList.emplace_back(line); + } + } + + std::vector includeList; + {// and whitelist + std::vector list = SplitLines(ReadFileAsString(includeListFile)); + for (const auto &line: list) { + if (!line.empty() && line[0] != '#') includeList.emplace_back(line); + } + } + + std::vector spritesToLoad; + {// populate spritesToLoad + Json::Reader reader; + std::filesystem::path assetPacksRoot(inDirectory); + for (const auto& dirEntry : std::filesystem::recursive_directory_iterator(inDirectory)) { + if (dirEntry.path().extension() == ".assetpack") { + std::string pngPathBase = dirEntry.path().parent_path().string(); + std::string spritePathBase = std::filesystem::relative(dirEntry.path(), assetPacksRoot).parent_path().string(); + std::replace(spritePathBase.begin(), spritePathBase.end(), '\\', '/'); + if (!pngPathBase.empty()) pngPathBase += "/"; + if (!spritePathBase.empty()) spritePathBase += "/"; + + std::string assetPack = ReadFileAsString(dirEntry.path().string()); + Json::Value root; + if (reader.parse(assetPack, root)) { + std::map pivotsMap; + for (const auto &pivot: root["pivots"]) { + pivotsMap[pivot["texPath"].asString()] = pivot["pivot"]; + } + for (const auto &mat: root["materials"]) { + const std::string spritePath = spritePathBase + mat["name"].asString(); + const std::string localMainTexPath = mat["mainTexPath"].asString(); + const std::string pngPath = pngPathBase + localMainTexPath; + Json::Value pivotJson = pivotsMap[localMainTexPath]; + vec2 pivot = {pivotJson["x"].asFloat(), pivotJson["y"].asFloat()}; + vec2 size = {mat["size"]["x"].asFloat(), mat["size"]["y"].asFloat()}; + + bool ignored = false; + // ignore list + for (const auto &ignore: ignoreList) { + if (std::regex_match(spritePath, ignore)) { + ignored = true; + break; + } + } + // include list + if (ignored) + for (const auto &include: includeList) { + if (std::regex_match(spritePath, include)) { + ignored = false; + break; + } + } + + LOG("%s%s", ignored ? "\t[ignored] " : "", spritePath.c_str()) + SpriteInfo info = { + .spritePath = spritePath, + .pngPath = pngPath, + .pivot = pivot.serialized(), + .size = size.serialized(), + .includeInPsd = !ignored + }; + spritesToLoad.push_back(info); + } + } else { + ERR("failed to parse json!") + } + } + } + LOG("======== %zu sprites ========", spritesToLoad.size()) + } + // with all the sprites to load, actually load them: + if (listOnly) { + LOG("(list only; skipping actual output..)") + } else { + LOG("(writing psd '%s')", outPsd.c_str()) + EchoesGatherSprites(spritesToLoad, outPsd); + } + +#endif + + LOG("done.") + return 0; +} diff --git a/src/Echoes/Log.h b/src/Echoes/Log.h new file mode 100644 index 0000000..0cc7ac9 --- /dev/null +++ b/src/Echoes/Log.h @@ -0,0 +1,132 @@ +#pragma once + +#include + +// for showing last relative_path node, see: https://stackoverflow.com/questions/8487986/file-macro-shows-full-path +#ifdef WINOS +#define PATH_ELIM_SLASH '\\' +#endif +#ifdef MACOS +#define PATH_ELIM_SLASH '/' +#endif +#define __FILENAME__ (strrchr(__FILE__, PATH_ELIM_SLASH) ? strrchr(__FILE__, PATH_ELIM_SLASH) + 1 : __FILE__) + +// colors +// for color formatting, see: https://stackoverflow.com/questions/2616906/how-do-i-output-coloured-text-to-a-linux-terminal +#define COLOR_RED printf("\033[31m"); +#define COLOR_GREEN printf("\033[32m"); +#define COLOR_YELLOW printf("\033[33m"); +#define COLOR_BLUE printf("\033[34m"); +#define COLOR_MAGENTA printf("\033[35m"); +#define COLOR_CYAN printf("\033[36m"); + +#define COLOR_RESET printf("\033[0m"); + +// helpful component(s) +#define LOCATION printf("%s: %d", __FILENAME__, __LINE__) + +// break (if using sdl) +#ifdef WINOS +//#include +#define DEBUG_BREAK __debugbreak(); +#else +#define DEBUG_BREAK ; +#endif + +// logging + +#ifdef WINOS +#define NEWLINE { printf("\n"); fflush(stdout); } +#endif + +#ifdef MACOS +#define NEWLINE printf("\n"); +#endif + +#define LOGR(...) { \ + printf(__VA_ARGS__); \ + NEWLINE \ +} + +#if DEBUG +#define LOG(...) { \ + COLOR_GREEN \ + printf("["); LOCATION; printf("] "); \ + COLOR_RESET \ + printf(__VA_ARGS__); \ + NEWLINE \ +} + +#define WARN(...) { \ + COLOR_YELLOW \ + printf("["); LOCATION; printf("] "); \ + COLOR_RESET \ + printf(__VA_ARGS__); \ + NEWLINE \ +} + +#define ERR(...) { \ + COLOR_RED \ + printf("["); LOCATION; printf("] "); \ + COLOR_RESET \ + printf(__VA_ARGS__); \ + NEWLINE \ + DEBUG_BREAK \ +} +#else +#define LOG(...) { \ + printf(__VA_ARGS__); \ + NEWLINE \ +} +#define WARN(...) { \ + printf("[WARNING] "); \ + printf(__VA_ARGS__); \ + NEWLINE \ +} + +#define ERR(...) { \ + printf("[ERROR] "); \ + printf(__VA_ARGS__); \ + NEWLINE \ +} +#endif + +// blue (assets) +#define ASSET(...) { \ + COLOR_BLUE \ + printf("[Asset] "); \ + printf(__VA_ARGS__); \ + COLOR_RESET \ + NEWLINE \ +} + +// assertions + +#if DEBUG + +#define EXPECT_M(STATEMENT, EXPECTED, ...) { \ + if (STATEMENT != EXPECTED) { \ + COLOR_RED \ + printf("["); LOCATION; printf("]"); \ + printf("[Assertion failed] "); \ + printf(__VA_ARGS__); \ + COLOR_RESET \ + NEWLINE \ + DEBUG_BREAK \ + } \ +} + +#define EXPECT(STATEMENT, EXPECTED) EXPECT_M(STATEMENT, EXPECTED, "") + +#define ASSERT_M(STATEMENT, ...) EXPECT_M(STATEMENT, true, __VA_ARGS__) + +#define ASSERT(STATEMENT) EXPECT(STATEMENT, true) + +#else + +#define EXPECT_M(STATEMENT, EXPECTED, ...) STATEMENT; +#define EXPECT(STATEMENT, EXPECTED) STATEMENT; +#define ASSERT_M(STATEMENT) ; +#define ASSERT(STATEMENT) ; + +#endif diff --git a/src/Echoes/MakeBlueprint.cpp b/src/Echoes/MakeBlueprint.cpp new file mode 100644 index 0000000..03afef0 --- /dev/null +++ b/src/Echoes/MakeBlueprint.cpp @@ -0,0 +1,142 @@ +// +// Created by miyehn on 1/1/2024. +// +#include "Log.h" +#include "Utils.h" + +#include "../Psd/Psd.h" +#include "../Psd/PsdMallocAllocator.h" +#include "../Psd/PsdLayerMaskSection.h" +#include "../Psd/PsdNativeFile.h" +#include "../Psd/PsdDocument.h" +#include "../Psd/PsdParseDocument.h" +#include "../Psd/PsdColorMode.h" +#include "../Psd/PsdLayer.h" +#include "../Psd/PsdParseLayerMaskSection.h" +#include "../Psd/PsdLayerCanvasCopy.h" +#include "../Psd/PsdChannel.h" +#include "../Psd/PsdInterleave.h" +#include "../Psd/PsdChannelType.h" +#include "../Psd/PsdLayerType.h" +#include "../Psd/PsdBlendMode.h" + +#include "cxxopts.hpp" +#include "../External/stb_image.h" +#include "json/json.h" + +namespace { +PSD_USING_NAMESPACE; +} + +int main(int argc, const char* argv[]) { + + cxxopts::Options options("EchoesMakeBlueprint", "Documentation: TODO\n"); + options.allow_unrecognised_options(); + +#if 1 + options.add_options() + ("f,file", "input .psd file, absolute path or relative to executable", cxxopts::value()/*->default_value("output")*/) + ("i,index", "input .index file, absolute path or relative to executable", cxxopts::value()/*->default_value("output")*/) + ("o,output", "output filename without extension, absolute path or relative to executable", cxxopts::value()/*->default_value("output")*/) + ; + auto result = options.parse(argc, argv); + + std::string inPsd, inIndex, outBlueprint; + try { + inPsd = result["file"].as(); + inIndex = result["index"].as(); + outBlueprint = result["output"].as() + ".islandblueprint"; + LOG("inPsd: %s, inIndex: %s, outBlueprint: %s", inPsd.c_str(), inIndex.c_str(), outBlueprint.c_str()) + } catch (std::exception &e) { + std::cout << options.help() << std::endl; + return 0; + } + + psd::Document* document; + psd::LayerMaskSection* section; + psd::MallocAllocator allocator; + psd::NativeFile file(&allocator); + if (!ReadPsdLayers(inPsd, document, section, allocator, file)) { + // early out + return 0; + } + + // build pivots map from index file + std::string indexStr = ReadFileAsString(inIndex); + Json::Reader reader; + Json::Value indexContent; + std::map> pivotsMap; + if (reader.parse(indexStr, indexContent)) { + for (const auto &sprite: indexContent) { + std::string spritePath = sprite["spritePath"].asString(); + vec2 pivot = {sprite["pivot"]["x"].asFloat(), sprite["pivot"]["y"].asFloat()}; + vec2 size = {sprite["size"]["x"].asFloat(), sprite["size"]["y"].asFloat()}; + pivotsMap[spritePath] = std::tuple(pivot, size); + } + LOG("loaded library index containing %zu sprites", pivotsMap.size()) + } else { + ERR("failed to parse index file") + return 0; + } + + // find origin's unit position + vec2 originUnitPos; + for (int i = 0; i < section->layerCount; i++) { + Layer *layer = §ion->layers[i]; + if (IsOrUnderLayer(layer, "meta")) { + if (GetName(layer) == "origin") { + vec2 pixelPos = vec2( + layer->left + float(layer->right - layer->left) * 0.5f, + layer->top + float(layer->bottom - layer->top) * 0.5f + ); + originUnitPos = PixelPosToUnitPos(pixelPos); + } + } + } + + LOG("origin at: %.3f, %.3f", originUnitPos.x, originUnitPos.y); + + // parse layout folder in psd, and add to output json + Json::Value bpRoot; + int spriteIdx = 0; + for (int i = 0; i < section->layerCount; i++) { + Layer* layer = §ion->layers[i]; + if (!IsOrUnderLayer(layer, "layout") || (layer->type != layerType::SECTION_DIVIDER && !VisibleInHierarchy(layer))) continue; + + // temp: for now, also exclude all groups and section dividers + if (tolower(GetName(layer))=="layout" && layer->type==layerType::OPEN_FOLDER || layer->type==layerType::CLOSED_FOLDER) continue; + if (layer->type == layerType::SECTION_DIVIDER) continue; + + std::string spritePath = SplitTokens(GetName(layer), '#')[0]; + trim(spritePath); + std::replace(spritePath.begin(), spritePath.end(), '&', '/'); + + vec2 pivot = std::get<0>(pivotsMap[spritePath]); + vec2 size = std::get<1>(pivotsMap[spritePath]); + float x2d = layer->left + (layer->right - layer->left) * pivot.x; + float y2d = layer->bottom - (layer->bottom - layer->top) * pivot.y; + vec2 unitPos = PixelPosToUnitPos({x2d, y2d}) - originUnitPos - size / 2; + + // add to json + Json::Value spriteJson; + spriteJson["spritePath"] = spritePath; + spriteJson["position"] = unitPos.serialized(); + bpRoot[spriteIdx] = spriteJson; + spriteIdx++; + + LOG("exported '%s'", spritePath.c_str()) + } + + file.Close(); + + // write file + std::stringstream stream; + stream << bpRoot; + if (!WriteStringToFile(outBlueprint, stream.str())) { + return 0; + } + +#endif + LOG("done.") + return 0; +} diff --git a/src/Echoes/Utils.cpp b/src/Echoes/Utils.cpp new file mode 100644 index 0000000..112652e --- /dev/null +++ b/src/Echoes/Utils.cpp @@ -0,0 +1,207 @@ +#include +#include + +#include "../Psd/Psd.h" +#include "../Psd/PsdMallocAllocator.h" +#include "../Psd/PsdLayerMaskSection.h" +#include "../Psd/PsdNativeFile.h" +#include "../Psd/PsdDocument.h" +#include "../Psd/PsdParseDocument.h" +#include "../Psd/PsdColorMode.h" +#include "../Psd/PsdLayer.h" +#include "../Psd/PsdParseLayerMaskSection.h" +#include "../Psd/PsdChannel.h" + +#include "Log.h" +#include "json/json.h" + +#include "Utils.h" + +std::string ReadFileAsString(const std::string& path) { + std::ifstream file(path, std::ios::ate); + EXPECT_M(file.is_open(), true, "failed to open file '%s'", path.c_str()) + size_t size = file.tellg(); + std::string outStr; + outStr.reserve(size); + file.seekg(0); + outStr.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + file.close(); + return outStr; +} + +bool WriteStringToFile(const std::string& path, const std::string& content) { + std::ofstream file; + file.open(path); + if (file.is_open()) { + file << content; + file.close(); + return true; + } else { + return false; + } +} +std::vector SplitLines(const std::string& str) { + std::vector result; + int start = 0, end; + do { + end = str.find('\n', start); + std::string s = str.substr(start, end - start); + trim(s); + result.push_back(s); + start = end + 1; + } while (end != -1); + + return result; +} +bool ReadPsdLayers( + const std::string& inFile, + psd::Document*& pDocument, + psd::LayerMaskSection*& pSection, + psd::MallocAllocator& allocator, + psd::NativeFile& file +){ + const std::wstring fullPath(inFile.c_str(), inFile.c_str() + inFile.length()); + + // open file + if (!file.OpenRead(fullPath.c_str())) { +#if ECHOES_EXPORTER_WINDOWED + AppendToGUILog({LT_ERROR, "ERROR: Failed to open file!"}); +#endif + ERR("failed to open file") + return false; + } + + // create document + psd::Document* document = psd::CreateDocument(&file, &allocator); + if (!document) { +#if ECHOES_EXPORTER_WINDOWED + AppendToGUILog({LT_ERROR, "ERROR: Failed to open file! Was that a PSD document?"}); +#endif + WARN("failed to create psd document") + file.Close(); + return false; + } + + // check color mode + if (document->colorMode != psd::colorMode::RGB) { +#if ECHOES_EXPORTER_WINDOWED + AppendToGUILog({LT_ERROR, "ERROR: This PSD is not in RGB color mode"}); +#endif + WARN("psd is not in RGB color mode") + file.Close(); + return false; + } + + // check bits per channel + if (document->bitsPerChannel != 8) { +#if ECHOES_EXPORTER_WINDOWED + AppendToGUILog({LT_ERROR, "ERROR: This PSD doesn't have 8 bits per channel"}); +#endif + WARN("this psd doesn't have 8 bits per channel") + file.Close(); + return false; + } + + // read section + auto section = psd::ParseLayerMaskSection(document, &file, &allocator); + if (!section) { +#if ECHOES_EXPORTER_WINDOWED + AppendToGUILog({LT_ERROR, "ERROR: failed to parse layer mask section"}); +#endif + WARN("failed to parse layer mask section") + file.Close(); + return false; + } + + pDocument = document; + pSection = section; + return true; +} +std::string GetName(const psd::Layer* layer) { + std::string name(layer->name.c_str()); + trim(name); + return name; +} + +bool IsOrUnderLayer(const psd::Layer* layer, const std::string& ancestorName) { + const psd::Layer* itr = layer; + while (itr) { + if (tolower(GetName(itr)) == tolower(ancestorName)) return true; + itr = itr->parent; + } + return false; +} +bool VisibleInHierarchy(const psd::Layer* layer) { + const psd::Layer* itr = layer; + while (itr) { + if (!itr->isVisible) return false; + itr = itr->parent; + } + return true; +} + +Json::Value vec2::serialized() const { + Json::Value result; + result["x"] = x; + result["y"] = y; + return result; +} +vec2 ToIsometric(vec2 p) { + vec2 v0 = ISO_X; + vec2 v1 = ISO_Y; + return { + v0.x * p.x + v1.x * p.y, + v0.y * p.x + v1.y * p.y + }; +} + +vec2 FromIsometric(vec2 p) { + const float det = ISO_X.x * ISO_Y.y - ISO_Y.x * ISO_X.y; + vec2 v0 = { + ISO_Y.y / det, + -ISO_X.y / det + }; + vec2 v1 = { + -ISO_Y.x / det, + ISO_X.x / det + }; + return { + v0.x * p.x + v1.x * p.y, + v0.y * p.x + v1.y * p.y + }; +} + +vec2 PixelPosToUnitPos(vec2 pixelPos, float workingPPDU) { + vec2 unitPos = FromIsometric(pixelPos); + return unitPos / (workingPPDU / sqrt2); +} + +vec2 UnitPosToPixelPos(vec2 unitPos, float workingPPDU) { + vec2 pixelPos = ToIsometric(unitPos); + return pixelPos * (workingPPDU / sqrt2); +} + +std::vector SplitTokens(const std::string& s, char separator) { + std::vector tokens; + std::string cur; + for (char c : s) { + if (c == separator) { + if (cur.length() > 0) tokens.push_back(cur); + cur = ""; + } else { + cur += c; + } + } + if (cur.length() > 0) tokens.push_back(cur); + return tokens; +} + +std::string JoinTokens(const std::vector &tokens) { + std::string result; + for (auto token : tokens) { + ASSERT(token.length() > 0) + token[0] = (char)toupper(token[0]); + result += token; + } + return result; +} diff --git a/src/Echoes/Utils.h b/src/Echoes/Utils.h new file mode 100644 index 0000000..4f06e6f --- /dev/null +++ b/src/Echoes/Utils.h @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include "json/json-forwards.h" + +#if ECHOES_EXPORTER_WINDOWED +enum GUILogType: int { + LT_LOG = 0, + LT_SUCCESS = 1, + LT_WARNING = 2, + LT_ERROR = 3 +}; +struct GUILogEntry { + GUILogType type; + std::string msg; +}; +void AppendToGUILog(const GUILogEntry &entry, bool clearFirst = false); +#endif + +// trim from start (in place) +inline void ltrim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); +} + +// trim from end (in place) +inline void rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), s.end()); +} + +// trim from both ends (in place) +inline void trim(std::string &s) { + rtrim(s); + ltrim(s); +} + +inline std::string tolower(const std::string& s) { + std::string result; + for (char c : s) { + result += std::tolower(c); + } + return result; +} + +std::string ReadFileAsString(const std::string& path); + +bool WriteStringToFile(const std::string& path, const std::string& content); + +std::vector SplitLines(const std::string& str); + +namespace psd { + struct Document; + struct LayerMaskSection; + struct MallocAllocator; + struct NativeFile; + struct Layer; +} +bool ReadPsdLayers( + const std::string& inFile, + psd::Document*& pDocument, + psd::LayerMaskSection*& pSection, + psd::MallocAllocator& allocator, + psd::NativeFile& file); + +std::string GetName(const psd::Layer* layer); + +bool IsOrUnderLayer(const psd::Layer* layer, const std::string& ancestorName); + +bool VisibleInHierarchy(const psd::Layer* layer); + +struct vec2 { + float x = 0; + float y = 0; + + vec2 operator+(const vec2 other) const { + return { + x + other.x, + y + other.y + }; + } + + vec2 operator-(const vec2 other) const { + return { + x - other.x, + y - other.y + }; + } + + vec2 operator*(float c) const { + return { x * c, y * c}; + } + vec2 operator/(float c) const { + return { x / c, y / c}; + } + Json::Value serialized() const; +}; + +struct ivec2 { + int x = 0; + int y = 0; + operator vec2() const { + return {(float)x, (float)y}; + } +}; + +const float sqrt2 = std::sqrt(2.0f); +const float sqrt3 = std::sqrt(3.0f); +const vec2 ISO_X = {sqrt2 / 2, sqrt2 / sqrt3 / 2 }; +const vec2 ISO_Y = {sqrt2 / 2, -sqrt2 / sqrt3 / 2 }; +const float STANDARD_PPDU = sqrt2 * 100; + +vec2 PixelPosToUnitPos(vec2 pixelPos, float workingPPDU = STANDARD_PPDU); + +vec2 UnitPosToPixelPos(vec2 unitPos, float workingPPDU = STANDARD_PPDU); + +std::vector SplitTokens(const std::string& s, char separator=' '); + +std::string JoinTokens(const std::vector &tokens); diff --git a/src/Echoes/WinMain.cpp b/src/Echoes/WinMain.cpp new file mode 100644 index 0000000..6ab6851 --- /dev/null +++ b/src/Echoes/WinMain.cpp @@ -0,0 +1,577 @@ +// +// Created by miyehn on 11/21/2023. +// +// HelloWindowsDesktop.cpp +// compile with: /D_UNICODE /DUNICODE /DWIN32 /D_WINDOWS /c + +#include +#include +#include +#include +#include +#include +#include "AssetPack.h" +#include "Log.h" +#include "Utils.h" + +// Global variables + +// The main window class name. +static TCHAR szWindowClass[] = _T("EchoesExporter"); +// The string that appears in the application's title bar. +#if DEBUG +static TCHAR szTitle[] = _T("Echoes Exporter (version: 2/22/24 DEBUG)"); +#else +static TCHAR szTitle[] = _T("Echoes Exporter (version: 2/22/24)"); +#endif + +// Stored instance handle for use in Win32 API calls such as FindResource +HINSTANCE hInst; + +// Forward declarations of functions included in this code module: +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +/////////////////////////////////////////////// + +#define CMD_DEBUG 1 +#define CMD_OPEN_PSD 2 +#define CMD_PSD_FORMATTING_DOC 3 +#define CMD_BROWSE_SAVE_DIRECTORY 4 +#define CMD_SPRITECONVERTER_DOC 5 +#define CMD_EXPORT_ASSETPACK 6 +#define CMD_RELOAD_LAST_PSD_AND_EXPORT 7 +#define CMD_SET_DRESSING_GUIDE_DOC 8 +#define CMD_EXPORT_LAYOUT_PNGS 9 + +#define WINDOW_WIDTH 640 +#define WINDOW_HEIGHT 720 +#define WINDOW_PADDING 10 +#define LOG_HEIGHT 200 +#define GAP_SMALL 5 +#define GAP_MEDIUM 10 +#define GAP_LARGE 20 + + +static std::string GUILog; +static HWND hLogWnd = NULL; +static HWND hRootWnd = NULL; +static HWND hSaveDirectory = NULL; +static HWND hAssetPackName = NULL; +void AppendToGUILog(const GUILogEntry &entry, bool clearFirst) { + if (clearFirst) GUILog = ""; + GUILog += entry.msg; + GUILog += "\r\n"; + if (!SetWindowText(hLogWnd, _T(GUILog.c_str()))) { + ERR("Failed to update log text") + } + InvalidateRect(hLogWnd, NULL, true); + UpdateWindow(hLogWnd); +} + +static AssetPack assetPack; + +static HWND hMaterialsList = NULL; +void AddMenus(HWND hWnd); +void AddContent(HWND hWnd); + +static GUID openPsdGUID = { + 1700158156, 55139, 19343, {137, 123, 65, 113} +}; +static GUID exportPathGUID = { + 1308487700, 50820, 18963, {143, 61, 205, 250} +}; + +bool ReadPsd(const std::string& InFilePath, bool useLastPath = false) { + assetPack = AssetPack(); // clear it first + static std::string lastFilePath; + std::string filePath; + if (useLastPath) { + filePath = lastFilePath; + } else { + filePath = InFilePath; + lastFilePath = InFilePath; + } + AppendToGUILog({LT_LOG, "Loading " + InFilePath}, true); + // load psd file content + bool success = EchoesReadPsdToAssetPack(filePath, assetPack); + if (success) { + SendMessage(hMaterialsList, LB_RESETCONTENT, NULL, NULL); + int spritesCount = 0; + for (auto& spritePair : assetPack.spriteSets) { + auto &sprite = spritePair.second; + std::string displayName = sprite.getBaseName(); + if (sprite.baseLayersData.size() > 1) { + displayName += " (" + std::to_string(sprite.baseLayersData.size()) + " parts)"; + } + if (sprite.emissionMaskData.size() > 0) { + displayName += " (w emission)"; + } + if (sprite.lightLayerNames.size() > 0) { + displayName += " (" + std::to_string(sprite.lightLayerNames.size()) + " light"; + if (sprite.lightLayerNames.size() > 1) { + displayName += "s"; + } + displayName += ")"; + } + int pos = SendMessage(hMaterialsList, LB_ADDSTRING, NULL, (LPARAM)displayName.c_str()); + SendMessage(hMaterialsList, LB_SETITEMDATA, pos, (LPARAM)spritesCount); + spritesCount++; + } + if (spritesCount > 0) { + AppendToGUILog({LT_LOG, "Loaded " + std::to_string(spritesCount) + " sprite(s)."}); + } else { + AppendToGUILog({LT_WARNING, "WARNING: no sprites are loaded. Are you sure the PSD file is formatted correctly?"}); + } + } else { + AppendToGUILog({LT_ERROR, "Failed to load PSD. Make sure your PSD is formatted correctly. If you're still not sure why, contact Rain."}); + } + InvalidateRect(hMaterialsList, NULL, true); + UpdateWindow(hMaterialsList); + return success; +} + +void CmdOpenPsd() { + static HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + EXPECT(SUCCEEDED(hr), true) + + IFileOpenDialog *pFileOpen; + EXPECT(SUCCEEDED(CoCreateInstance( + CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, reinterpret_cast(&pFileOpen))), true); + pFileOpen->SetClientGuid(openPsdGUID); + + const COMDLG_FILTERSPEC fileTypes[] = {{L"PSD documents (*.psd)", L"*.psd"}}; + if (!SUCCEEDED(pFileOpen->SetFileTypes(ARRAYSIZE(fileTypes), fileTypes))) return; + + if (SUCCEEDED(pFileOpen->Show(NULL))) { + IShellItem* pItem; + if (SUCCEEDED(pFileOpen->GetResult(&pItem))) { + PWSTR pszFilePath; + EXPECT(SUCCEEDED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath)), true) + + std::wstring ws(pszFilePath); + std::string s(ws.begin(), ws.end()); + LOG("reading PSD: %s", s.c_str()) + + ASSERT(hMaterialsList != NULL) + ReadPsd(s); + } + } +} + +void CmdBrowseSaveDirectory() { + static HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + EXPECT(SUCCEEDED(hr), true) + + IFileOpenDialog *pFileDialog; + if (!SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileDialog)))) return; + pFileDialog->SetClientGuid(exportPathGUID); + + DWORD curOptions; + if (!SUCCEEDED(pFileDialog->GetOptions(&curOptions))) return; + if (!SUCCEEDED(pFileDialog->SetOptions(curOptions | FOS_PICKFOLDERS))) return; + + if (SUCCEEDED(pFileDialog->Show(NULL))) { + IShellItem* pItem; + if (SUCCEEDED(pFileDialog->GetResult(&pItem))) { + PWSTR pszFilePath; + EXPECT(SUCCEEDED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath)), true) + std::wstring ws(pszFilePath); + std::string s(ws.begin(), ws.end()); + LOG("save path: %s", s.c_str()) + if (hSaveDirectory != NULL) { + SetWindowText(hSaveDirectory, _T(s.c_str())); + InvalidateRect(hSaveDirectory, NULL, true); + UpdateWindow(hSaveDirectory); + } + } + } +} + +void ShowMessage(const std::string& text, const std::string& caption) { + int msgbox = MessageBoxA( + hRootWnd, + _T(text.c_str()), + _T(caption.c_str()), + MB_ICONWARNING | MB_OK); + + switch(msgbox) + { + case IDOK: + break; + default: + break; + } +} + +void CmdExportAssetPack(ExportType exportType) { + if (assetPack.spriteSets.size() == 0) { + ShowMessage( "Did you load the PSD file correctly?", "No sprites to export"); + return; + } + + TCHAR buf[512]; + GetWindowText(hSaveDirectory, buf, 512); + std::string outDirectory(buf); + DWORD ftyp = GetFileAttributesA(outDirectory.c_str()); + if (ftyp == INVALID_FILE_ATTRIBUTES || !(ftyp & FILE_ATTRIBUTE_DIRECTORY)) { + ShowMessage( "You need to first set export destination by clicking \"Browse..\"", "Invalid destination"); + return; + } + GetWindowText(hAssetPackName, buf, 512); + std::string assetPackName(buf); + trim(assetPackName); + if (assetPackName.length() == 0) { + ShowMessage( "Please specify a non-empty asset pack name, something like \"DarkwoodRocks\"", "Invalid asset pack name"); + return; + } + if (exportType == ET_ASSETPACK) { + std::string fullPath = outDirectory + "\\" + assetPackName; + if (ExportAssetPack(assetPack, outDirectory, assetPackName, 0, ET_ASSETPACK)) { + AppendToGUILog({LT_SUCCESS, "Exported " + std::to_string(assetPack.spriteSets.size()) + " sprites to:"}); + AppendToGUILog({LT_SUCCESS, fullPath}); + AppendToGUILog({LT_LOG, "If you have build access, continue by following \"Help -> Configuring sprites in Unity\". Otherwise, submit the above folder to our google drive."}); + } else { + AppendToGUILog({LT_ERROR, "Failed to export asset pack. Check the warnings/errors above (if any). Still not sure why? DM me (Rain) your psd and let me take a look"}); + } + } else if (exportType == ET_LAYOUT) { + std::string fullPath = outDirectory + "\\" + assetPackName + " (layout)"; + if (ExportAssetPack(assetPack, outDirectory, assetPackName, 0, ET_LAYOUT)) { + AppendToGUILog({LT_SUCCESS, "Saved " + std::to_string(assetPack.spriteSets.size()) + " pngs to:"}); + AppendToGUILog({LT_SUCCESS, fullPath}); + AppendToGUILog({LT_LOG, "Now you can continue the \"Add new props\" section in \"Help -> Set dressing guide\"."}); + } else { + AppendToGUILog({LT_ERROR, "Failed to save pngs. Check the warnings/errors above (if any). Still not sure why? DM me (Rain) your psd and let me take a look"}); + } + } +} + +void CmdPsdFormattingDoc() { + ShellExecute(0, 0, _T("https://docs.google.com/document/d/1rUcAj-sK-fXPAnCBTwqrQdLIPcEDpZkJkXL9nP_7WYQ/edit?usp=sharing"), 0, 0, SW_SHOW); +} + +void CmdSpriteConverterDoc() { + ShellExecute(0, 0, _T("https://docs.google.com/document/d/1PGAtx2exCd0odrkpP0Dy70wVBlfR2gZaDSlqFCF5uHM/edit?usp=sharing"), 0, 0, SW_SHOW); +} + +void CmdSetDressingGuideDoc() { + ShellExecute(0, 0, _T("https://docs.google.com/document/d/1iX-HU8rwNFsydFe0JVBnDGkLsOtvlaf0TSqxx3WIjeM/edit?usp=sharing"), 0, 0, SW_SHOW); +} + +void CmdReloadLastPsdAndExport() { + if (ReadPsd("", true)) { + CmdExportAssetPack(ET_ASSETPACK); + } +} + +void TestCommand() { + +} + +int WINAPI WinMain( + _In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, + _In_ LPSTR lpCmdLine, + _In_ int nCmdShow +) +{ + WNDCLASSEX wcex; + + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(wcex.hInstance, IDI_APPLICATION); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = szWindowClass; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + + if (!RegisterClassEx(&wcex)) + { + MessageBox(NULL, + _T("Call to RegisterClassEx failed!"), + _T("Windows Desktop Guided Tour"), + NULL); + + return 1; + } + + // Store instance handle in our global variable + hInst = hInstance; + + // The parameters to CreateWindowEx explained: + // WS_EX_OVERLAPPEDWINDOW : An optional extended window style. + // szWindowClass: the name of the application + // szTitle: the text that appears in the title bar + // WS_OVERLAPPEDWINDOW: the type of window to create + // CW_USEDEFAULT, CW_USEDEFAULT: initial position (x, y) + // 500, 100: initial size (width, length) + // NULL: the parent of this window + // NULL: this application does not have a menu bar + // hInstance: the first parameter from WinMain + // NULL: not used in this application + HWND hWnd = CreateWindowEx( + 0, + szWindowClass, + szTitle, + WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME ^ WS_MAXIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + WINDOW_WIDTH, WINDOW_HEIGHT, + NULL, + NULL, + hInstance, + NULL + ); + + if (!hWnd) + { + MessageBox(NULL, + _T("Call to CreateWindow failed!"), + _T("Windows Desktop Guided Tour"), + NULL); + + return 1; + } + hRootWnd = hWnd; + + AddMenus(hWnd); + AddContent(hWnd); + + ShowWindow(hWnd, nCmdShow); + InvalidateRect(hWnd, NULL, true); + UpdateWindow(hWnd); + + // Main message loop: + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return (int)msg.wParam; +} + +// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM) +// +// PURPOSE: Processes messages for the main window. +// +// WM_PAINT - Paint the main window +// WM_DESTROY - post a quit message and return +LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + PAINTSTRUCT ps; + HDC hdc; + switch (message) + { + case WM_COMMAND: + switch (wParam) + { + case CMD_DEBUG: + TestCommand(); + break; + case CMD_OPEN_PSD: + CmdOpenPsd(); + break; + case CMD_PSD_FORMATTING_DOC: + CmdPsdFormattingDoc(); + break; + case CMD_SET_DRESSING_GUIDE_DOC: + CmdSetDressingGuideDoc(); + break; + case CMD_BROWSE_SAVE_DIRECTORY: + CmdBrowseSaveDirectory(); + break; + case CMD_EXPORT_ASSETPACK: + CmdExportAssetPack(ET_ASSETPACK); + break; + case CMD_EXPORT_LAYOUT_PNGS: + CmdExportAssetPack(ET_LAYOUT); + break; + case CMD_SPRITECONVERTER_DOC: + CmdSpriteConverterDoc(); + break; + case CMD_RELOAD_LAST_PSD_AND_EXPORT: + CmdReloadLastPsdAndExport(); + break; + } + break; + case WM_DROPFILES: + { + HDROP hDrop = reinterpret_cast(wParam); + TCHAR fileName[512]; + uint32_t filesCount = DragQueryFile(hDrop, -1, fileName, 1024); + if (filesCount && DragQueryFile(hDrop, 0, fileName, 1024)) { + std::string s(fileName); + ReadPsd(s); + } + break; + } + case WM_DESTROY: + { + // delete solid brush? + PostQuitMessage(0); + break; + } + default: + return DefWindowProc(hWnd, message, wParam, lParam); + break; + } + return 0; +} + +void AddMenus(HWND hWnd) { + + HMENU hFileMenu = CreateMenu(); + AppendMenu(hFileMenu, MF_STRING, CMD_OPEN_PSD, "Open"); + //AppendMenu(hFileMenu, MF_STRING, CMD_BROWSE_SAVE_DIRECTORY, "Export"); + + HMENU hHelpMenu = CreateMenu(); + AppendMenu(hHelpMenu, MF_STRING, CMD_PSD_FORMATTING_DOC, "Formatting PSD for export"); + AppendMenu(hHelpMenu, MF_STRING, CMD_SPRITECONVERTER_DOC, "Configuring sprites in Unity"); + AppendMenu(hHelpMenu, MF_STRING, CMD_SET_DRESSING_GUIDE_DOC, "Set dressing guide"); + + HMENU hMenu = CreateMenu(); + AppendMenu(hMenu, MF_POPUP, (UINT_PTR)hFileMenu, "File"); + AppendMenu(hMenu, MF_POPUP, (UINT_PTR)hHelpMenu, "Help"); + + SetMenu(hWnd, hMenu); +} + +void AddContent(HWND hWnd) { + + const uint32_t horizontalPadding = GAP_MEDIUM; + const uint32_t contentWidth = WINDOW_WIDTH - 2*horizontalPadding - 2*WINDOW_PADDING; + + uint32_t currentHeight = GAP_MEDIUM; + // info + EXPECT(CreateWindowEx( + 0, WC_EDIT, _T( + "1) Make sure you have your PSD formatted correctly\r\n" + " - see \"Help -> Formatting PSD for export\" for instructions\r\n" + "2) Select your PSD from \"File -> Open\" or drag it to the box below\r\n" + "3) Specify export destination and asset pack name, and hit \"Export\"\r\n" + " - or use the buttons below for other actions\r\n" + "4) Submit the entire exported folder by following instructions in the output log\r\n" + "\r\n" + "At or DM Rain (discord: @miyehn) for questions or feedback or anything else!" + ), + WS_VISIBLE | WS_CHILD | ES_READONLY | ES_MULTILINE, + horizontalPadding, currentHeight, contentWidth, 130, + hWnd, nullptr, nullptr, nullptr + ) != nullptr, true) + currentHeight += 130 + GAP_MEDIUM; + + // list (title) + EXPECT(CreateWindowEx( + 0, WC_STATIC, _T( + "Sprites to export:" + ), + WS_VISIBLE | WS_CHILD, + horizontalPadding, currentHeight, contentWidth, 20, + hWnd, nullptr, nullptr, nullptr + ) != nullptr, true) + currentHeight += 20; + // list (content) + hMaterialsList = CreateWindowEx( + WS_EX_ACCEPTFILES, WC_LISTBOX, _T("materials list"), + WS_VISIBLE | WS_CHILD | WS_BORDER | LBS_NOSEL | WS_VSCROLL, + horizontalPadding, currentHeight, contentWidth, 170, + hWnd, nullptr, nullptr, nullptr + ); + EXPECT(hMaterialsList != nullptr, true) + currentHeight += 170; + + const uint32_t descrLen = 140; + const uint32_t btnLen = 100; + { // save destination + EXPECT(CreateWindowEx( + 0, WC_STATIC, _T( + "Export destination:" + ), + WS_VISIBLE | WS_CHILD, + horizontalPadding, currentHeight, descrLen, 20, + hWnd, nullptr, nullptr, nullptr + ) != nullptr, true) + hSaveDirectory = CreateWindowEx( + 0, WC_EDIT, _T(""), + WS_VISIBLE | WS_CHILD | ES_READONLY | ES_AUTOHSCROLL, + horizontalPadding + descrLen, currentHeight, contentWidth-descrLen-btnLen-GAP_SMALL, 20, + hWnd, nullptr, nullptr, nullptr); + EXPECT(hSaveDirectory != nullptr, true) + EXPECT(CreateWindowEx( + 0, WC_BUTTON, _T("Browse.."), + WS_VISIBLE | WS_CHILD, + WINDOW_WIDTH-2*WINDOW_PADDING-horizontalPadding-btnLen, currentHeight, btnLen, 20, + hWnd, (HMENU)CMD_BROWSE_SAVE_DIRECTORY, nullptr, nullptr + ) != nullptr, true) + currentHeight += 20; + } + {// asset pack name + EXPECT(CreateWindowEx( + 0, WC_STATIC, _T( + "Asset pack name:" + ), + WS_VISIBLE | WS_CHILD, + horizontalPadding, currentHeight, descrLen, 20, + hWnd, nullptr, nullptr, nullptr + ) != nullptr, true) + hAssetPackName = CreateWindowEx( + 0, WC_EDIT, _T(""), + WS_VISIBLE | WS_CHILD | ES_AUTOHSCROLL | WS_BORDER, + horizontalPadding + descrLen, currentHeight, contentWidth-descrLen-btnLen-GAP_SMALL, 20, + hWnd, nullptr, nullptr, nullptr); + EXPECT(hAssetPackName != nullptr, true) + EXPECT(CreateWindowEx( + 0, WC_BUTTON, _T("Export"), + WS_VISIBLE | WS_CHILD, + WINDOW_WIDTH-2*WINDOW_PADDING-horizontalPadding-btnLen, currentHeight, btnLen, 20, + hWnd, (HMENU)CMD_EXPORT_ASSETPACK, nullptr, nullptr + ) != nullptr, true) + currentHeight += 20 + GAP_MEDIUM; + } + + {// Reload last PSD document and export + EXPECT(CreateWindowEx( + 0, WC_STATIC, _T("Or:"), + WS_VISIBLE | WS_CHILD, + horizontalPadding, currentHeight, 60, 20, + hWnd, nullptr, nullptr, nullptr + ) != nullptr, true) + const int btnWidth = (contentWidth - 60) / 2; + EXPECT(CreateWindowEx( + 0, WC_BUTTON, _T("Reload last PSD and re-export"), + WS_VISIBLE | WS_CHILD, + 60 + horizontalPadding, currentHeight, btnWidth - 2, 20, + hWnd, (HMENU)CMD_RELOAD_LAST_PSD_AND_EXPORT, nullptr, nullptr + ) != nullptr, true) + EXPECT(CreateWindowEx( + 0, WC_BUTTON, _T("Generate PNGs for set dressing"), + WS_VISIBLE | WS_CHILD, + 60 + horizontalPadding + btnWidth, currentHeight, btnWidth, 20, + hWnd, (HMENU)CMD_EXPORT_LAYOUT_PNGS, nullptr, nullptr + ) != nullptr, true) + currentHeight += 20 + GAP_MEDIUM; + } + + // log (title) + EXPECT(CreateWindowEx( + 0, WC_STATIC, _T("Output Log"), + WS_VISIBLE | WS_CHILD, + horizontalPadding, currentHeight, contentWidth, 20, + hWnd, nullptr, nullptr, nullptr + ) != nullptr, true) + currentHeight += 20; + + // log (content) + hLogWnd = CreateWindowEx( + 0, WC_EDIT, _T(""), + WS_VISIBLE | WS_CHILD | ES_READONLY | ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL | WS_BORDER, + horizontalPadding, currentHeight, contentWidth, LOG_HEIGHT, + hWnd, nullptr, nullptr, nullptr + ); + EXPECT(hLogWnd!=nullptr, true) +} \ No newline at end of file diff --git a/src/External/cxxopts.hpp b/src/External/cxxopts.hpp new file mode 100644 index 0000000..b789a5c --- /dev/null +++ b/src/External/cxxopts.hpp @@ -0,0 +1,2828 @@ +/* + +Copyright (c) 2014-2022 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// vim: ts=2:sw=2:expandtab + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CXXOPTS_NO_EXCEPTIONS +#include +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +# define CXXOPTS_NO_REGEX true +# endif +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern +#define CXXOPTS_LINKONCE __declspec(selectany) extern +#else +#define CXXOPTS_LINKONCE_CONST +#define CXXOPTS_LINKONCE +#endif + +#ifndef CXXOPTS_NO_REGEX +# include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +# if __has_include() +# include +# ifdef __cpp_lib_optional +# define CXXOPTS_HAS_OPTIONAL +# endif +# endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif + +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' +#endif + +#define CXXOPTS__VERSION_MAJOR 3 +#define CXXOPTS__VERSION_MINOR 1 +#define CXXOPTS__VERSION_PATCH 1 + +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 + #define CXXOPTS_NULL_DEREF_IGNORE +#endif + +#if defined(__GNUC__) +#define DO_PRAGMA(x) _Pragma(#x) +#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) +#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) +#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) +#else +// define other compilers here if needed +#define CXXOPTS_DIAGNOSTIC_PUSH +#define CXXOPTS_DIAGNOSTIC_POP +#define CXXOPTS_IGNORE_WARNING(x) +#endif + +#ifdef CXXOPTS_NO_RTTI +#define CXXOPTS_RTTI_CAST static_cast +#else +#define CXXOPTS_RTTI_CAST dynamic_cast +#endif + +namespace cxxopts { +static constexpr struct { + uint8_t major, minor, patch; +} version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH +}; +} // namespace cxxopts + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts { + +using String = icu::UnicodeString; + +inline +String +toLocalString(std::string s) +{ + return icu::UnicodeString::fromUTF8(std::move(s)); +} + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") +// This will be ignored under other compilers like LLVM clang. +class UnicodeStringIterator +{ + public: + + using iterator_category = std::forward_iterator_tag; + using value_type = int32_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; +}; +CXXOPTS_DIAGNOSTIC_POP + +inline +String& +stringAppend(String&s, String a) +{ + return s.append(std::move(a)); +} + +inline +String& +stringAppend(String& s, std::size_t n, UChar32 c) +{ + for (std::size_t i = 0; i != n; ++i) + { + s.append(c); + } + + return s; +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; +} + +inline +std::size_t +stringLength(const String& s) +{ + return s.length(); +} + +inline +std::string +toUTF8String(const String& s) +{ + std::string result; + s.toUTF8String(result); + + return result; +} + +inline +bool +empty(const String& s) +{ + return s.isEmpty(); +} + +} // namespace cxxopts + +namespace std { + +inline +cxxopts::UnicodeStringIterator +begin(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, 0); +} + +inline +cxxopts::UnicodeStringIterator +end(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, s.length()); +} + +} // namespace std + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts { + +using String = std::string; + +template +T +toLocalString(T&& t) +{ + return std::forward(t); +} + +inline +std::size_t +stringLength(const String& s) +{ + return s.length(); +} + +inline +String& +stringAppend(String&s, const String& a) +{ + return s.append(a); +} + +inline +String& +stringAppend(String& s, std::size_t n, char c) +{ + return s.append(n, c); +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + return s.append(begin, end); +} + +template +std::string +toUTF8String(T&& t) +{ + return std::forward(t); +} + +inline +bool +empty(const std::string& s) +{ + return s.empty(); +} + +} // namespace cxxopts + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts { + +namespace { +#ifdef _WIN32 +CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); +CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); +#else +CXXOPTS_LINKONCE_CONST std::string LQUOTE("‘"); +CXXOPTS_LINKONCE_CONST std::string RQUOTE("’"); +#endif +} // namespace + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we +// want to silence it: warning: base class 'class +// std::enable_shared_from_this' has accessible non-virtual +// destructor This will be ignored under other compilers like LLVM clang. +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + +// some older versions of GCC warn under this warning +CXXOPTS_IGNORE_WARNING("-Weffc++") +class Value : public std::enable_shared_from_this +{ + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; +}; + +CXXOPTS_DIAGNOSTIC_POP + +namespace exceptions { + +class exception : public std::exception +{ + public: + explicit exception(std::string message) + : m_message(std::move(message)) + { + } + + CXXOPTS_NODISCARD + const char* + what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; +}; + +class specification : public exception +{ + public: + + explicit specification(const std::string& message) + : exception(message) + { + } +}; + +class parsing : public exception +{ + public: + explicit parsing(const std::string& message) + : exception(message) + { + } +}; + +class option_already_exists : public specification +{ + public: + explicit option_already_exists(const std::string& option) + : specification("Option " + LQUOTE + option + RQUOTE + " already exists") + { + } +}; + +class invalid_option_format : public specification +{ + public: + explicit invalid_option_format(const std::string& format) + : specification("Invalid option format " + LQUOTE + format + RQUOTE) + { + } +}; + +class invalid_option_syntax : public parsing { + public: + explicit invalid_option_syntax(const std::string& text) + : parsing("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") + { + } +}; + +class no_such_option : public parsing +{ + public: + explicit no_such_option(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } +}; + +class missing_argument : public parsing +{ + public: + explicit missing_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) + { + } +}; + +class option_requires_argument : public parsing +{ + public: + explicit option_requires_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) + { + } +}; + +class gratuitous_argument_for_option : public parsing +{ + public: + gratuitous_argument_for_option + ( + const std::string& option, + const std::string& arg + ) + : parsing( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } +}; + +class requested_option_not_present : public parsing +{ + public: + explicit requested_option_not_present(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " not present") + { + } +}; + +class option_has_no_value : public exception +{ + public: + explicit option_has_no_value(const std::string& option) + : exception( + !option.empty() ? + ("Option " + LQUOTE + option + RQUOTE + " has no value") : + "Option has no value") + { + } +}; + +class incorrect_argument_type : public parsing +{ + public: + explicit incorrect_argument_type + ( + const std::string& arg + ) + : parsing( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } +}; + +} // namespace exceptions + + +template +void throw_or_mimic(const std::string& text) +{ + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{text}; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{text}; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +#endif +} + +using OptionNames = std::vector; + +namespace values { + +namespace parser_tool { + +struct IntegerDesc +{ + std::string negative = ""; + std::string base = ""; + std::string value = ""; +}; +struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; +}; + +#ifdef CXXOPTS_NO_REGEX +inline IntegerDesc SplitInteger(const std::string &text) +{ + if (text.empty()) + { + throw_or_mimic(text); + } + IntegerDesc desc; + const char *pdata = text.c_str(); + if (*pdata == '-') + { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) + { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') + { + desc.value = std::string(pdata); + } + else + { + throw_or_mimic(text); + } + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') + { + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) + { + return true; + } + } + else if (strncmp(pdata, "1\0", 2) == 0) + { + return true; + } + return false; +} + +inline bool IsFalseText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') + { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) + { + return true; + } + } + else if (strncmp(pdata, "0\0", 2) == 0) + { + return true; + } + return false; +} + +inline OptionNames split_option_names(const std::string &text) +{ + OptionNames split_names; + + std::string::size_type token_start_pos = 0; + auto length = text.length(); + + if (length == 0) + { + throw_or_mimic(text); + } + + while (token_start_pos < length) { + const auto &npos = std::string::npos; + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); + if (next_non_space_pos == npos) { + throw_or_mimic(text); + } + token_start_pos = next_non_space_pos; + auto next_delimiter_pos = text.find(',', token_start_pos); + if (next_delimiter_pos == token_start_pos) { + throw_or_mimic(text); + } + if (next_delimiter_pos == npos) { + next_delimiter_pos = length; + } + auto token_length = next_delimiter_pos - token_start_pos; + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ + { + const char* option_name_valid_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-.?"; + + if (!std::isalnum(text[token_start_pos], std::locale::classic()) || + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) { + throw_or_mimic(text); + } + } + split_names.emplace_back(text.substr(token_start_pos, token_length)); + token_start_pos = next_delimiter_pos + 1; + } + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + ArguDesc argu_desc; + const char *pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) + { + pdata += 2; + if (isalnum(*pdata, std::locale::classic())) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || *pdata == '_') + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) + { + if (*pdata == '=') + { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') + { + argu_desc.value = std::string(pdata); + } + matched = true; + } + else if (*pdata == '\0') + { + matched = true; + } + } + } + } + else if (strncmp(pdata, "-", 1) == 0) + { + pdata += 1; + argu_desc.grouping = true; + while (isalnum(*pdata, std::locale::classic())) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + matched = !argu_desc.arg_name.empty() && *pdata == '\0'; + } + return argu_desc; +} + +#else // CXXOPTS_NO_REGEX + +namespace { +CXXOPTS_LINKONCE +std::basic_regex integer_pattern + ("(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"); +CXXOPTS_LINKONCE +std::basic_regex truthy_pattern + ("(t|T)(rue)?|1"); +CXXOPTS_LINKONCE +std::basic_regex falsy_pattern + ("(f|F)(alse)?|0"); +CXXOPTS_LINKONCE +std::basic_regex option_matcher + ("--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"); +CXXOPTS_LINKONCE +std::basic_regex option_specifier + ("([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"); +CXXOPTS_LINKONCE +std::basic_regex option_specifier_separator(", *"); + +} // namespace + +inline IntegerDesc SplitInteger(const std::string &text) +{ + std::smatch match; + std::regex_match(text, match, integer_pattern); + + if (match.length() == 0) + { + throw_or_mimic(text); + } + + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; + + if (match.length(4) > 0) + { + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + std::smatch result; + std::regex_match(text, result, truthy_pattern); + return !result.empty(); +} + +inline bool IsFalseText(const std::string &text) +{ + std::smatch result; + std::regex_match(text, result, falsy_pattern); + return !result.empty(); +} + +// Gets the option names specified via a single, comma-separated string, +// and returns the separate, space-discarded, non-empty names +// (without considering which or how many are single-character) +inline OptionNames split_option_names(const std::string &text) +{ + if (!std::regex_match(text.c_str(), option_specifier)) + { + throw_or_mimic(text); + } + + OptionNames split_names; + + constexpr int use_non_matches { -1 }; + auto token_iterator = std::sregex_token_iterator( + text.begin(), text.end(), option_specifier_separator, use_non_matches); + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names)); + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) + { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); + } + } + + return argu_desc; +} + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX +} // namespace parser_tool + +namespace detail { + +template +struct SignedCheck; + +template +struct SignedCheck +{ + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } + } + else + { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } + } + } +}; + +template +struct SignedCheck +{ + template + void + operator()(bool, U, const std::string&) const {} +}; + +template +void +check_signed_range(bool negative, U value, const std::string& text) +{ + SignedCheck::is_signed>()(negative, value, text); +} + +} // namespace detail + +template +void +checked_negate(R& r, T&& t, const std::string&, std::true_type) +{ + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + r = static_cast(-static_cast(t-1)-1); +} + +template +void +checked_negate(R&, T&&, const std::string& text, std::false_type) +{ + throw_or_mimic(text); +} + +template +void +integer_parser(const std::string& text, T& value) +{ + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + + using US = typename std::make_unsigned::type; + constexpr bool is_signed = std::numeric_limits::is_signed; + + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string & value_match = int_desc.value; + + US result = 0; + + for (char ch : value_match) + { + US digit = 0; + + if (ch >= '0' && ch <= '9') + { + digit = static_cast(ch - '0'); + } + else if (base == 16 && ch >= 'a' && ch <= 'f') + { + digit = static_cast(ch - 'a' + 10); + } + else if (base == 16 && ch >= 'A' && ch <= 'F') + { + digit = static_cast(ch - 'A' + 10); + } + else + { + throw_or_mimic(text); + } + + const US next = static_cast(result * base + digit); + if (result > next) + { + throw_or_mimic(text); + } + + result = next; + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + checked_negate(value, result, text, std::integral_constant()); + } + else + { + value = static_cast(result); + } +} + +template +void stringstream_parser(const std::string& text, T& value) +{ + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } +} + +template ::value>::type* = nullptr + > +void parse_value(const std::string& text, T& value) +{ + integer_parser(text, value); +} + +inline +void +parse_value(const std::string& text, bool& value) +{ + if (parser_tool::IsTrueText(text)) + { + value = true; + return; + } + + if (parser_tool::IsFalseText(text)) + { + value = false; + return; + } + + throw_or_mimic(text); +} + +inline +void +parse_value(const std::string& text, std::string& value) +{ + value = text; +} + +// The fallback parser. It uses the stringstream parser to parse all types +// that have not been overloaded explicitly. It has to be placed in the +// source code before all other more specialized templates. +template ::value>::type* = nullptr + > +void +parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); +} + +template +void +parse_value(const std::string& text, std::vector& value) +{ + if (text.empty()) { + T v; + parse_value(text, v); + value.emplace_back(std::move(v)); + return; + } + std::stringstream in(text); + std::string token; + while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + T v; + parse_value(token, v); + value.emplace_back(std::move(v)); + } +} + +#ifdef CXXOPTS_HAS_OPTIONAL +template +void +parse_value(const std::string& text, std::optional& value) +{ + T result; + parse_value(text, result); + value = std::move(result); +} +#endif + +inline +void parse_value(const std::string& text, char& c) +{ + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; +} + +template +struct type_is_container +{ + static constexpr bool value = false; +}; + +template +struct type_is_container> +{ + static constexpr bool value = true; +}; + +template +class abstract_value : public Value +{ + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + explicit abstract_value(T* t) + : m_store(t) + { + } + + ~abstract_value() override = default; + + abstract_value& operator=(const abstract_value&) = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + parse(const std::string& text) const override + { + parse_value(text, *m_store); + } + + bool + is_container() const override + { + return type_is_container::value; + } + + void + parse() const override + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const override + { + return m_default; + } + + bool + has_implicit() const override + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) override + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) override + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() override + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const override + { + return m_default_value; + } + + std::string + get_implicit_value() const override + { + return m_implicit_value; + } + + bool + is_boolean() const override + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + return *m_store; + } + + protected: + std::shared_ptr m_result{}; + T* m_store{}; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value{}; + std::string m_implicit_value{}; +}; + +template +class standard_value : public abstract_value +{ + public: + using abstract_value::abstract_value; + + CXXOPTS_NODISCARD + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } +}; + +template <> +class standard_value : public abstract_value +{ + public: + ~standard_value() override = default; + + standard_value() + { + set_default_and_implicit(); + } + + explicit standard_value(bool* b) + : abstract_value(b) + { + m_implicit = true; + m_implicit_value = "true"; + } + + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } +}; + +} // namespace values + +template +std::shared_ptr +value() +{ + return std::make_shared>(); +} + +template +std::shared_ptr +value(T& t) +{ + return std::make_shared>(&t); +} + +class OptionAdder; + +CXXOPTS_NODISCARD +inline +const std::string& +first_or_empty(const OptionNames& long_names) +{ + static const std::string empty{""}; + return long_names.empty() ? empty : long_names.front(); +} + +class OptionDetails +{ + public: + OptionDetails + ( + std::string short_, + OptionNames long_, + String desc, + std::shared_ptr val + ) + : m_short(std::move(short_)) + , m_long(std::move(long_)) + , m_desc(std::move(desc)) + , m_value(std::move(val)) + , m_count(0) + { + m_hash = std::hash{}(first_long_name() + m_short); + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_value(rhs.m_value->clone()) + , m_count(rhs.m_count) + { + } + + OptionDetails(OptionDetails&& rhs) = default; + + CXXOPTS_NODISCARD + const String& + description() const + { + return m_desc; + } + + CXXOPTS_NODISCARD + const Value& + value() const { + return *m_value; + } + + CXXOPTS_NODISCARD + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + CXXOPTS_NODISCARD + const std::string& + short_name() const + { + return m_short; + } + + CXXOPTS_NODISCARD + const std::string& + first_long_name() const + { + return first_or_empty(m_long); + } + + CXXOPTS_NODISCARD + const std::string& + essential_name() const + { + return m_long.empty() ? m_short : m_long.front(); + } + + CXXOPTS_NODISCARD + const OptionNames & + long_names() const + { + return m_long; + } + + std::size_t + hash() const + { + return m_hash; + } + + private: + std::string m_short{}; + OptionNames m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; + int m_count; + + std::size_t m_hash{}; +}; + +struct HelpOptionDetails +{ + std::string s; + OptionNames l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; +}; + +struct HelpGroupDetails +{ + std::string name{}; + std::string description{}; + std::vector options{}; +}; + +class OptionValue +{ + public: + void + parse + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + m_long_names = &details->long_names(); + } + + void + parse_default(const std::shared_ptr& details) + { + ensure_value(details); + m_default = true; + m_long_names = &details->long_names(); + m_value->parse(); + } + + void + parse_no_value(const std::shared_ptr& details) + { + m_long_names = &details->long_names(); + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnull-dereference") +#endif + + CXXOPTS_NODISCARD + std::size_t + count() const noexcept + { + return m_count; + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_POP +#endif + + // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD + bool + has_default() const noexcept + { + return m_default; + } + + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic( + m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); + } + + return CXXOPTS_RTTI_CAST&>(*m_value).get(); + } + + private: + void + ensure_value(const std::shared_ptr& details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + + const OptionNames * m_long_names = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + std::size_t m_count = 0; + bool m_default = false; +}; + +class KeyValue +{ + public: + KeyValue(std::string key_, std::string value_) + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + CXXOPTS_NODISCARD + const std::string& + key() const + { + return m_key; + } + + CXXOPTS_NODISCARD + const std::string& + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; +}; + +using ParsedHashMap = std::unordered_map; +using NameHashMap = std::unordered_map; + +class ParseResult +{ + public: + class Iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + +// GCC complains about m_iter not being initialised in the member +// initializer list +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Weffc++") + Iterator(const ParseResult *pr, bool end=false) + : m_pr(pr) + { + if (end) + { + m_sequential = false; + m_iter = m_pr->m_defaults.end(); + } + else + { + m_sequential = true; + m_iter = m_pr->m_sequential.begin(); + + if (m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + } + } + } +CXXOPTS_DIAGNOSTIC_POP + + Iterator& operator++() + { + ++m_iter; + if(m_sequential && m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; + } + + Iterator operator++(int) + { + Iterator retval = *this; + ++(*this); + return retval; + } + + bool operator==(const Iterator& other) const + { + return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); + } + + bool operator!=(const Iterator& other) const + { + return !(*this == other); + } + + const KeyValue& operator*() + { + return *m_iter; + } + + const KeyValue* operator->() + { + return m_iter.operator->(); + } + + private: + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + bool m_sequential = true; + }; + + ParseResult() = default; + ParseResult(const ParseResult&) = default; + + ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, + std::vector default_opts, std::vector&& unmatched_args) + : m_keys(std::move(keys)) + , m_values(std::move(values)) + , m_sequential(std::move(sequential)) + , m_defaults(std::move(default_opts)) + , m_unmatched(std::move(unmatched_args)) + { + } + + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; + + Iterator + begin() const + { + return Iterator(this); + } + + Iterator + end() const + { + return Iterator(this, true); + } + + std::size_t + count(const std::string& o) const + { + auto iter = m_keys.find(o); + if (iter == m_keys.end()) + { + return 0; + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + return 0; + } + + return viter->second.count(); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_keys.find(option); + + if (iter == m_keys.end()) + { + throw_or_mimic(option); + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + throw_or_mimic(option); + } + + return viter->second; + } + + const std::vector& + arguments() const + { + return m_sequential; + } + + const std::vector& + unmatched() const + { + return m_unmatched; + } + + const std::vector& + defaults() const + { + return m_defaults; + } + + const std::string + arguments_string() const + { + std::string result; + for(const auto& kv: m_sequential) + { + result += kv.key() + " = " + kv.value() + "\n"; + } + for(const auto& kv: m_defaults) + { + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; + } + return result; + } + + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; +}; + +struct Option +{ + Option + ( + std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "" + ) + : opts_(std::move(opts)) + , desc_(std::move(desc)) + , value_(std::move(value)) + , arg_help_(std::move(arg_help)) + { + } + + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; +}; + +using OptionMap = std::unordered_map>; +using PositionalList = std::vector; +using PositionalListIterator = PositionalList::const_iterator; + +class OptionParser +{ + public: + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options) + , m_positional(positional) + , m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult + parse(int argc, const char* const* argv); + + bool + consume_positional(const std::string& a, PositionalListIterator& next); + + void + checked_parse_arg + ( + int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name + ); + + void + add_to_option(OptionMap::const_iterator iter, const std::string& option, const std::string& arg); + + void + parse_option + ( + const std::shared_ptr& value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(const std::shared_ptr& details); + + void + parse_no_value(const std::shared_ptr& details); + + private: + + void finalise_aliases(); + + const OptionMap& m_options; + const PositionalList& m_positional; + + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; + + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; +}; + +class Options +{ + public: + + explicit Options(std::string program_name, std::string help_string = "") + : m_program(std::move(program_name)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_width(76) + , m_tab_expansion(false) + , m_options(std::make_shared()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } + + Options& + set_width(std::size_t width) + { + m_width = width; + return *this; + } + + Options& + set_tab_expansion(bool expansion=true) + { + m_tab_expansion = expansion; + return *this; + } + + ParseResult + parse(int argc, const char* const* argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_options + ( + const std::string& group, + std::initializer_list