diff --git a/assets/opensb/items/armors/avian/avian-tier6separator/old/bsleeve.png b/assets/opensb/items/armors/avian/avian-tier6separator/old/bsleeve.png new file mode 100644 index 0000000..7d52748 Binary files /dev/null and b/assets/opensb/items/armors/avian/avian-tier6separator/old/bsleeve.png differ diff --git a/assets/opensb/items/armors/avian/avian-tier6separator/old/chestf.png b/assets/opensb/items/armors/avian/avian-tier6separator/old/chestf.png new file mode 100644 index 0000000..5779930 Binary files /dev/null and b/assets/opensb/items/armors/avian/avian-tier6separator/old/chestf.png differ diff --git a/assets/opensb/items/armors/avian/avian-tier6separator/old/chestm.png b/assets/opensb/items/armors/avian/avian-tier6separator/old/chestm.png new file mode 100644 index 0000000..4073c18 Binary files /dev/null and b/assets/opensb/items/armors/avian/avian-tier6separator/old/chestm.png differ diff --git a/assets/opensb/items/armors/avian/avian-tier6separator/old/fsleeve.png b/assets/opensb/items/armors/avian/avian-tier6separator/old/fsleeve.png new file mode 100644 index 0000000..db91be2 Binary files /dev/null and b/assets/opensb/items/armors/avian/avian-tier6separator/old/fsleeve.png differ diff --git a/source/frontend/StarClientCommandProcessor.cpp b/source/frontend/StarClientCommandProcessor.cpp index 5b2ca0f..78e1ee9 100644 --- a/source/frontend/StarClientCommandProcessor.cpp +++ b/source/frontend/StarClientCommandProcessor.cpp @@ -441,10 +441,152 @@ String ClientCommandProcessor::respawnInWorld(String const& argumentsString) { return strf("Respawn in this world set to {} (This is client-side!)", respawnInWorld ? "true" : "false"); } -// Temporary hardcoded render command for debugging purposes, future version will write to the clipboard -String ClientCommandProcessor::render(String const& imagePath) { - auto image = Root::singleton().assets()->image(imagePath); - image->writePng(File::open("render.png", IOMode::Write)); +// Hardcoded render command, future version will write to the clipboard and possibly be implemented in Lua +String ClientCommandProcessor::render(String const& path) { + if (path.empty()) { + return "Specify a path to render an image, or for worn armor: " + "^cyan;hat^reset;/^cyan;chest^reset;/^cyan;legs^reset;/^cyan;back^reset; " + "or for body parts: " + "^cyan;head^reset;/^cyan;body^reset;/^cyan;hair^reset;/^cyan;facialhair^reset;/" + "^cyan;facialmask^reset;/^cyan;frontarm^reset;/^cyan;backarm^reset;/^cyan;emote^reset;"; + } + AssetPath assetPath; + bool outputSheet = false; + String outputName = "render"; + auto player = m_universeClient->mainPlayer(); + if (player && path.utf8Size() < 100) { + auto args = m_parser.tokenizeToStringList(path); + auto first = args.maybeFirst().value().toLower(); + auto humanoid = player->humanoid(); + auto& identity = humanoid->identity(); + auto species = identity.imagePath.value(identity.species); + outputSheet = true; + outputName = first; + if (first.equals("hat")) { + assetPath.basePath = humanoid->headArmorFrameset(); + assetPath.directives += humanoid->headArmorDirectives(); + } else if (first.equals("chest")) { + if (args.size() <= 1) { + return "Chest armors have multiple spritesheets. Do: " + "^white;/chest torso ^cyan;front^reset;/^cyan;torso^reset;/^cyan;back^reset;. " + "To repair old generated clothes, then also specify ^cyan;old^reset;."; + } + String sheet = args[1].toLower(); + outputName += " " + sheet; + if (sheet == "torso") { + assetPath.basePath = humanoid->chestArmorFrameset(); + assetPath.directives += humanoid->chestArmorDirectives(); + } else if (sheet == "front") { + assetPath.basePath = humanoid->frontSleeveFrameset(); + assetPath.directives += humanoid->chestArmorDirectives(); + } else if (sheet == "back") { + assetPath.basePath = humanoid->backSleeveFrameset(); + assetPath.directives += humanoid->chestArmorDirectives(); + } else { + return strf("^red;Invalid chest sheet type '{}'^reset;", sheet); + } + // recovery for custom chests made by a very old generator + if (args.size() <= 2 && args[2].toLower() == "old" && assetPath.basePath.beginsWith("/items/armors/avian/avian-tier6separator/")) + assetPath.basePath = "/items/armors/avian/avian-tier6separator/old/" + assetPath.basePath.substr(41); + } else if (first.equals("legs")) { + assetPath.basePath = humanoid->legsArmorFrameset(); + assetPath.directives += humanoid->legsArmorDirectives(); + } else if (first.equals("back")) { + assetPath.basePath = humanoid->backArmorFrameset(); + assetPath.directives += humanoid->backArmorDirectives(); + } else if (first.equals("body")) { + assetPath.basePath = humanoid->getBodyFromIdentity(); + assetPath.directives += identity.bodyDirectives; + } else if (first.equals("head")) { + outputSheet = false; + assetPath.basePath = humanoid->getHeadFromIdentity(); + assetPath.directives += identity.bodyDirectives; + } else if (first.equals("hair")) { + outputSheet = false; + assetPath.basePath = humanoid->getHairFromIdentity(); + assetPath.directives += identity.hairDirectives; + } else if (first.equals("facialhair")) { + outputSheet = false; + assetPath.basePath = humanoid->getFacialHairFromIdentity(); + assetPath.directives += identity.facialHairDirectives; + } else if (first.equals("facialmask")) { + outputSheet = false; + assetPath.basePath = humanoid->getFacialMaskFromIdentity(); + assetPath.directives += identity.facialMaskDirectives; + } else if (first.equals("frontarm")) { + assetPath.basePath = humanoid->getFrontArmFromIdentity(); + assetPath.directives += identity.bodyDirectives; + } else if (first.equals("backarm")) { + assetPath.basePath = humanoid->getBackArmFromIdentity(); + assetPath.directives += identity.bodyDirectives; + } else if (first.equals("emote")) { + assetPath.basePath = humanoid->getFacialEmotesFromIdentity(); + assetPath.directives += identity.emoteDirectives; + } else { + outputName = "render"; + } + + if (!outputSheet) + assetPath.subPath = String("normal"); + } + if (assetPath == AssetPath()) { + assetPath = AssetPath::split(path); + if (!assetPath.basePath.beginsWith("/")) + assetPath.basePath = "/assetmissing.png" + assetPath.basePath; + } + auto assets = Root::singleton().assets(); + ImageConstPtr image; + if (outputSheet) { + auto sheet = make_shared(*assets->image(assetPath.basePath)); + sheet->convert(PixelFormat::RGBA32); + AssetPath framePath = assetPath; + + StringMap> frames; + auto imageFrames = assets->imageFrames(assetPath.basePath); + for (auto& pair : imageFrames->frames) + frames[pair.first] = make_pair(pair.second, ImageConstPtr()); + + if (frames.empty()) + return "^red;Failed to save image^reset;"; + + for (auto& entry : frames) { + framePath.subPath = entry.first; + entry.second.second = assets->image(framePath); + } + + Vec2U frameSize = frames.begin()->second.first.size(); + Vec2U imageSize = frames.begin()->second.second->size().piecewiseMin(Vec2U{256, 256}); + if (imageSize.min() == 0) + return "^red;Resulting image is empty^reset;"; + + for (auto& frame : frames) { + RectU& box = frame.second.first; + box.setXMin((box.xMin() / frameSize[0]) * imageSize[0]); + box.setYMin(((sheet->height() - box.yMin() - box.height()) / frameSize[1]) * imageSize[1]); + box.setXMax(box.xMin() + imageSize[0]); + box.setYMax(box.yMin() + imageSize[1]); + } + + if (frameSize != imageSize) { + unsigned sheetWidth = (sheet->width() / frameSize[0]) * imageSize[0]; + unsigned sheetHeight = (sheet->height() / frameSize[0]) * imageSize[0]; + sheet->reset(sheetWidth, sheetHeight, PixelFormat::RGBA32); + } + + for (auto& entry : frames) + sheet->copyInto(entry.second.first.min(), *entry.second.second); + + image = std::move(sheet); + } else { + image = assets->image(assetPath); + } + if (image->size().min() == 0) + return "^red;Resulting image is empty^reset;"; + auto outputDirectory = Root::singleton().toStoragePath("output"); + auto outputPath = File::relativeTo(outputDirectory, strf("{}.png", outputName)); + if (!File::isDirectory(outputDirectory)) + File::makeDirectory(outputDirectory); + image->writePng(File::open(outputPath, IOMode::Write | IOMode::Truncate)); return strf("Saved {}x{} image to render.png", image->width(), image->height()); } diff --git a/source/game/StarHumanoid.cpp b/source/game/StarHumanoid.cpp index 97b90cd..2acc632 100644 --- a/source/game/StarHumanoid.cpp +++ b/source/game/StarHumanoid.cpp @@ -390,6 +390,46 @@ void Humanoid::setHelmetMaskDirectives(Directives helmetMaskDirectives) { m_helmetMaskDirectives = std::move(helmetMaskDirectives); } +Directives const& Humanoid::headArmorDirectives() const { + return m_headArmorDirectives; +}; + +String const& Humanoid::headArmorFrameset() const { + return m_headArmorFrameset; +}; + +Directives const& Humanoid::chestArmorDirectives() const { + return m_chestArmorDirectives; +}; + +String const& Humanoid::chestArmorFrameset() const { + return m_chestArmorFrameset; +}; + +String const& Humanoid::backSleeveFrameset() const { + return m_backSleeveFrameset; +}; + +String const& Humanoid::frontSleeveFrameset() const { + return m_frontSleeveFrameset; +}; + +Directives const& Humanoid::legsArmorDirectives() const { + return m_legsArmorDirectives; +}; + +String const& Humanoid::legsArmorFrameset() const { + return m_legsArmorFrameset; +}; + +Directives const& Humanoid::backArmorDirectives() const { + return m_backArmorDirectives; +}; + +String const& Humanoid::backArmorFrameset() const { + return m_backArmorFrameset; +}; + void Humanoid::setBodyHidden(bool hidden) { m_bodyHidden = hidden; } diff --git a/source/game/StarHumanoid.hpp b/source/game/StarHumanoid.hpp index 98412f2..d43c5ef 100644 --- a/source/game/StarHumanoid.hpp +++ b/source/game/StarHumanoid.hpp @@ -158,6 +158,18 @@ public: void setHelmetMaskDirectives(Directives helmetMaskDirectives); + // Getters for all of the above + Directives const& headArmorDirectives() const; + String const& headArmorFrameset() const; + Directives const& chestArmorDirectives() const; + String const& chestArmorFrameset() const; + String const& backSleeveFrameset() const; + String const& frontSleeveFrameset() const; + Directives const& legsArmorDirectives() const; + String const& legsArmorFrameset() const; + Directives const& backArmorDirectives() const; + String const& backArmorFrameset() const; + void setBodyHidden(bool hidden); void setState(State state); @@ -247,6 +259,16 @@ public: List particles(String const& name) const; Json const& defaultMovementParameters() const; + + String getHeadFromIdentity() const; + String getBodyFromIdentity() const; + String getFacialEmotesFromIdentity() const; + String getHairFromIdentity() const; + String getFacialHairFromIdentity() const; + String getFacialMaskFromIdentity() const; + String getBackArmFromIdentity() const; + String getFrontArmFromIdentity() const; + String getVaporTrailFrameset() const; // Extracts scalenearest from directives and returns the combined scale and // a new Directives without those scalenearest directives. @@ -269,16 +291,6 @@ private: String frameBase(State state) const; String emoteFrameBase(HumanoidEmote state) const; - String getHeadFromIdentity() const; - String getBodyFromIdentity() const; - String getFacialEmotesFromIdentity() const; - String getHairFromIdentity() const; - String getFacialHairFromIdentity() const; - String getFacialMaskFromIdentity() const; - String getBackArmFromIdentity() const; - String getFrontArmFromIdentity() const; - String getVaporTrailFrameset() const; - Directives getBodyDirectives() const; Directives getHairDirectives() const; Directives getEmoteDirectives() const;