From 5159b073bd9e3d2b903df27188b6b42db1ac65c7 Mon Sep 17 00:00:00 2001 From: Kae <80987908+Novaenia@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:41:42 +1100 Subject: [PATCH] /render: support for rendering out character and clothing sheets --- .../avian-tier6separator/old/bsleeve.png | Bin 0 -> 2488 bytes .../avian/avian-tier6separator/old/chestf.png | Bin 0 -> 557 bytes .../avian/avian-tier6separator/old/chestm.png | Bin 0 -> 606 bytes .../avian-tier6separator/old/fsleeve.png | Bin 0 -> 2320 bytes .../frontend/StarClientCommandProcessor.cpp | 150 +++++++++++++++++- source/game/StarHumanoid.cpp | 40 +++++ source/game/StarHumanoid.hpp | 32 ++-- 7 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 assets/opensb/items/armors/avian/avian-tier6separator/old/bsleeve.png create mode 100644 assets/opensb/items/armors/avian/avian-tier6separator/old/chestf.png create mode 100644 assets/opensb/items/armors/avian/avian-tier6separator/old/chestm.png create mode 100644 assets/opensb/items/armors/avian/avian-tier6separator/old/fsleeve.png 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 0000000000000000000000000000000000000000..7d527485d705d293be0febcba808b9f8818457df GIT binary patch literal 2488 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..db91be2858d7d1f22a4a402eebcf49bcc1b94064 GIT binary patch literal 2320 zcmV+r3GeoaP)hKmY&$!|1tO00001bW%=J06^y0W&i*Prb$FWRCwC$-7#+) zOR~lBe<1dW4LyB<3qrY}$6WR`6ptYA#k_oD#wV+*IiyLR@->XB>O^yH*S_z>zC5L?oJw^aI<#E zi{4sygPaCKOTRlE7(SAqS_yDEh zK5lNZEANu(YHVK+HrqESrz=&(DkzEJ=Kj03+sHg|L|tA@uK$HRCEE z#E0qE=c4RbyzwIMb#;G=^Fj#)67uo=^2N>XRd0*PdBS0vgg^onCU>l!O}6#BBFGa? zsuU0?z=c8Vs~rsOxmKK5^V;EI;xD8$!dPu~Xn9-2yCReilOhaT)fz^5TxkGSnI%-q5yNCTRI+tIZDiD!wzq)%E9rqWWleliDKoyZ1%-yrHf< z+H?NQw(aMDZTbK8^<|sv&YNNv#`ut5cvkIG(iZT~-B9boc>0`QcwX5ko<0q=E{w

KDAuIsw4>%uIZB`KC40kdG?#YeD+`6F~)Z(xAA z8yM#2U!R#Rqk*B_Q#ZYVfeE*pe3m@DfnoZLUENQQF!PM<^9bwNBeaPT#x>K`8yLph z;^R2h2ir~+0VGg*I$3WCNUqF}LF*zG>P;L});1 zX%5tk4_w~TOg}@tl%?5{$WL-F)R()Vv}c|+T&Q+^J(Tv$YT!bx9≥vm~pbzGoJk zs_VM0>$W@)V7BLrxv&q5H$f-rxst_$W?KaSp} zRV2w)FrP6QYxbaiGKb27vA^Q{HuBd+J8D>4!<>o z`u8tDyQO~Yd?53l2nGy3I??Ay;rkbo(H4Tym?Z451LLC;{o?khZ{>E+fs-&8Tv^># zE9O>D|FdH`vcJQA`n=)1+IB+!LH2jpk2fca5C&|HeEB?ly#FBkJM7gka3V&5T{!t3 zA8P#vXa76wz`}m@@ZHoVJU>g`a!yVouo{XJJ}^5Y+a~en9Nu@G0s_LwER#S0#$urY z1qfsoN(dvcSSDEn7M58m0S41`T{ow&&7uhm;AKZ}h*)-nAC~)-j1a`7MCiJ%GaxTMg2j?q(O2Ou{aNU`uIsw4>$ZY1_&eZAqqguLtsn@gg)8^V1$s(90LD8BcrflM}XOJDINO5e&wGWTMj1VDU4n-xI+E z$#hNev%$ewA;OBnzeaXI*Q|nW>iU=dkHP5pRLcm= ztW=4BWd&grC|;auDJZskC}v#+OW2aI1pwh6C0uQG@Qe{~62ZEV2#V4*vVyRL6#x)^ z-bl7)*ho|8jVb=drISfS-Y2k3^G9g+`7XAPr&~q>@X>Ml{UuJK30npW^Gry9EahD! zGig~{g@x@v@p}ZLK!K&aqhKcO>zX`5@NyLd2*CKbi==Ag6vn@H9Qp0v3B=LyC@ApD zowS?z(QoyQkhd<;ZfA*5-v|YVNe>=Oci3?z^Zx=yQ{Nr3pFIKs5Wj2Rc9e~`%*`k; zzgykiP}WWpVSJ!WfOui`X`pVu;{?(cwww$oj#goyuAL;p;|-H+ncdTd6DmCU)2#;1 qB;Rw3vih`PZhaNl(sf;Tq5cJ;#DfF>!cwgO0000image(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;