From df428ea40c1580fe6422a1d230e33b6121204215 Mon Sep 17 00:00:00 2001 From: Alpha Date: Mon, 4 Aug 2025 17:15:29 -0400 Subject: [PATCH 01/13] Wasm: implement file header reader --- wasm/src/.clang-format | 137 ++++++++++ wasm/src/.gitignore | 1 + wasm/src/Makefile | 31 +++ wasm/src/Reader.cpp | 119 +++++++++ wasm/src/Reader.h | 29 +++ wasm/src/World.h | 194 ++++++++++++++ wasm/src/WorldLoader.cpp | 512 +++++++++++++++++++++++++++++++++++++ wasm/src/ieee754_types.hpp | 169 ++++++++++++ 8 files changed, 1192 insertions(+) create mode 100644 wasm/src/.clang-format create mode 100644 wasm/src/.gitignore create mode 100644 wasm/src/Makefile create mode 100644 wasm/src/Reader.cpp create mode 100644 wasm/src/Reader.h create mode 100644 wasm/src/World.h create mode 100644 wasm/src/WorldLoader.cpp create mode 100644 wasm/src/ieee754_types.hpp diff --git a/wasm/src/.clang-format b/wasm/src/.clang-format new file mode 100644 index 0000000..2cc3824 --- /dev/null +++ b/wasm/src/.clang-format @@ -0,0 +1,137 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Linux +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + - Regex: '.*' + Priority: 1 + SortPriority: 0 +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +... diff --git a/wasm/src/.gitignore b/wasm/src/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/wasm/src/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/wasm/src/Makefile b/wasm/src/Makefile new file mode 100644 index 0000000..b8eb130 --- /dev/null +++ b/wasm/src/Makefile @@ -0,0 +1,31 @@ +# Config values + +CXXFLAGS := -Wall -Wextra -pedantic -Werror -std=c++20 -O2 +LDFLAGS := -O2 + +SRCS := Reader.cpp WorldLoader.cpp +OUT := terramap + +BUILD_DIR := build + + +# Build rules. + +CPPFLAGS := -Isrc + +OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) + +$(BUILD_DIR)/$(OUT): $(OBJS) + $(CXX) $(OBJS) -o $@ $(LDFLAGS) + +$(BUILD_DIR)/%.cpp.o: %.cpp + @mkdir -p $(dir $@) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -r $(BUILD_DIR) + +format: + clang-format -i $(SRCS) + +.PHONY: clean format diff --git a/wasm/src/Reader.cpp b/wasm/src/Reader.cpp new file mode 100644 index 0000000..c862679 --- /dev/null +++ b/wasm/src/Reader.cpp @@ -0,0 +1,119 @@ +#include "Reader.h" +#include "ieee754_types.hpp" + +#include + +typedef IEEE_754::_2008::Binary<32> float32_t; +typedef IEEE_754::_2008::Binary<64> float64_t; + +namespace +{ + +template +NumberType readLittleEndian(const char *buffer) +{ + NumberType result = 0; + for (int i = byteCount - 1; i >= 0; --i) { + result <<= 8; + result |= static_cast(buffer[i]); + } + return result; +} + +} // namespace + +Reader::Reader(const std::string &d) : pos(0), data(d) {} + +std::vector Reader::getBitVec() +{ + int len = getUint16(); + std::vector result; + uint8_t buf = 0; + for (int i = 0, offset = 0; i < len; ++i, ++offset) { + if (offset % 8 == 0) { + buf = getUint8(); + offset = 0; + } + result.push_back((buf & (1 << offset)) != 0); + } + return result; +} + +bool Reader::getBool() +{ + return getUint8() != 0; +} + +double Reader::getFloat32() +{ + return std::bit_cast(getUint32()); +} + +double Reader::getFloat64() +{ + return std::bit_cast(getUint64()); +} + +std::string Reader::getString() +{ + if (pos + 1 > data.size()) { + return {}; + } + // LEB128 encoded length prefix. + size_t len = 0; + size_t shift = 0; + uint8_t b; + do { + b = getUint8(); + len |= (b & 0x7f) << shift; + shift += 7; + } while ((b & 0x80) != 0); + std::string result = data.substr(pos, len); + pos += len; + return result; +} + +uint8_t Reader::getUint8() +{ + if (pos + 1 > data.size()) { + return {}; + } + uint8_t result = data[pos]; + pos += 1; + return result; +} + +uint16_t Reader::getUint16() +{ + if (pos + 2 > data.size()) { + return {}; + } + uint16_t result = readLittleEndian(data.c_str() + pos); + pos += 2; + return result; +} + +uint32_t Reader::getUint32() +{ + if (pos + 4 > data.size()) { + return {}; + } + uint32_t result = readLittleEndian(data.c_str() + pos); + pos += 4; + return result; +} + +uint64_t Reader::getUint64() +{ + if (pos + 8 > data.size()) { + return {}; + } + uint64_t result = readLittleEndian(data.c_str() + pos); + pos += 8; + return result; +} + +void Reader::skipBytes(size_t len) +{ + pos += len; +} diff --git a/wasm/src/Reader.h b/wasm/src/Reader.h new file mode 100644 index 0000000..0b352c1 --- /dev/null +++ b/wasm/src/Reader.h @@ -0,0 +1,29 @@ +#ifndef READER_H +#define READER_H + +#include +#include +#include + +class Reader +{ +private: + size_t pos; + const std::string &data; + +public: + Reader(const std::string &d); + + std::vector getBitVec(); + bool getBool(); + double getFloat32(); + double getFloat64(); + std::string getString(); + uint8_t getUint8(); + uint16_t getUint16(); + uint32_t getUint32(); + uint64_t getUint64(); + void skipBytes(size_t len); +}; + +#endif // READER_H diff --git a/wasm/src/World.h b/wasm/src/World.h new file mode 100644 index 0000000..7a9ceaa --- /dev/null +++ b/wasm/src/World.h @@ -0,0 +1,194 @@ +#ifndef WORLD_H +#define WORLD_H + +#include +#include +#include + +class World +{ +public: + int version; + int revision; + bool isFavorite; + std::vector framedTiles; + + std::string name; + std::string seed; + std::string generatorVersion; + std::string guid; + int id; + int left; + int right; + int top; + int bottom; + int height; + int width; + int gameMode; + bool drunkWorld; + bool forTheWorthy; + bool celebrationmk10; + bool theConstant; + bool notTheBees; + bool dontDigUp; + bool noTraps; + bool getFixedBoi; + std::string creationTime; + + int moonType; + std::array treeStyleCoords; + std::array treeStyles; + std::array caveStyleCoords; + std::array caveStyles; + int iceStyle; + int jungleStyle; + int underworldStyle; + std::array spawn; + double undergroundLevel; + double cavernLevel; + double gameTime; + bool isDay; + int moonPhase; + bool bloodMoon; + bool eclipse; + std::array dungeon; + bool isCrimson; + + bool downedEyeOfCthulu; + bool downedEaterOfWorlds; + bool downedSkeletron; + bool downedQueenBee; + bool downedTheDestroyer; + bool downedTheTwins; + bool downedSkeletronPrime; + bool downedAnyHardmodeBoss; + bool downedPlantera; + bool downedGolem; + bool downedSlimeKing; + bool savedGoblinTinkerer; + bool savedWizard; + bool savedMechanic; + bool defeatedGoblinInvasion; + bool downedClown; + bool defeatedFrostLegion; + bool defeatedPirates; + + bool brokeAShadowOrb; + bool meteorSpawned; + int shadowOrbsBroken; + int altarsSmashed; + bool hardMode; + bool partyOfDoom; + int goblinInvasionDelay; + int goblinInvasionSize; + int goblinInvasionType; + double goblinInvasionX; + double slimeRainTime; + int sundialCooldown; + bool raining; + int rainTimeLeft; + double maxRain; + int cobaltVariant; + int mythrilVariant; + int adamantiteVariant; + int forestStyle1; + int corruptionStyle; + int undergroundJungleStyle; + int snowStyle; + int hallowStyle; + int crimsonStyle; + int desertStyle; + int oceanStyle; + int cloudBackground; + int numberOfClouds; + double windSpeed; + + std::vector anglersFinishedDailyQuest; + bool savedAngler; + int anglerQuest; + bool savedStylist; + bool savedTaxCollector; + bool savedGolfer; + int invasionStartSize; + int cultistDelay; + std::vector enemyKillTallies; + bool fastForwardTimeToDawn; + + bool downedFishron; + bool downedMartians; + bool downedLunaticCultist; + bool downedMoonlord; + bool downedHalloweenPumpking; + bool downedHalloweenMourningWood; + bool downedChristmasIceQueen; + bool downedChristmasSantaNK1; + bool downedChristmasEverscream; + bool downedTowerSolar; + bool downedTowerVortex; + bool downedTowerNebula; + bool downedTowerStardust; + bool towerActiveSolar; + bool towerActiveVortex; + bool towerActiveNebula; + bool towerActiveStardust; + bool lunarApocalypseIsUp; + + bool partyManual; + bool partyGenuine; + int partyCooldown; + std::vector partyingNPCs; + bool sandstormActive; + int sandstormTimeLeft; + double sandstormSeverity; + double sandstormIntendedSeverity; + bool savedBartender; + bool downedInvasionTier1; + bool downedInvasionTier2; + bool downedInvasionTier3; + int mushroomStyle; + int underworldStyle2; + int forestStyle2; + int forestStyle3; + int forestStyle4; + bool combatBookUsed; + int lanternNightCooldown; + bool lanternNightGenuine; + bool lanternNightManual; + bool lanternNightNextIsGenuine; + std::vector treeTopVariations; + bool forceHalloween; + bool forceChristmas; + int copperVariant; + int ironVariant; + int silverVariant; + int goldVariant; + bool boughtCat; + bool boughtDog; + bool boughtBunny; + + bool downedEmpressOfLight; + bool downedQueenSlime; + bool downedDeerclops; + bool unlockedSlimeBlue; + bool unlockedMerchant; + bool unlockedDemolitionist; + bool unlockedPartyGirl; + bool unlockedDyeTrader; + bool unlockedTruffle; + bool unlockedArmsDealer; + bool unlockedNurse; + bool unlockedPrincess; + bool combatBookVolumeTwoUsed; + bool peddlersSatchelUsed; + bool unlockedSlimeGreen; + bool unlockedSlimeOld; + bool unlockedSlimePurple; + bool unlockedSlimeRainbow; + bool unlockedSlimeRed; + bool unlockedSlimeYellow; + bool unlockedSlimeCopper; + bool fastForwardTimeToDusk; + int moondialCooldown; +}; + +#endif // WORLD_H diff --git a/wasm/src/WorldLoader.cpp b/wasm/src/WorldLoader.cpp new file mode 100644 index 0000000..c4d0a86 --- /dev/null +++ b/wasm/src/WorldLoader.cpp @@ -0,0 +1,512 @@ +#include "Reader.h" +#include "World.h" + +#include +#include + +#ifndef __EMSCRIPTEN__ +void printWorld(World &world); +#endif + +std::string parseBinaryTime(uint64_t ticks) +{ + uint64_t ms = ticks / 10000 - 62135596800000ull; + auto time = std::chrono::sys_time{std::chrono::milliseconds{ms}}; + return std::format("{:%e %B %Y}", time); +} + +void readProperties(Reader &r, World &world) +{ + world.version = r.getUint32(); + r.skipBytes(8); + world.revision = r.getUint32(); + world.isFavorite = r.getBool(); + r.skipBytes(7); + r.skipBytes(4 * r.getUint16()); + world.framedTiles = r.getBitVec(); + + world.name = r.getString(); + if (world.version >= 179) { + world.seed = world.version == 179 ? std::to_string(r.getUint32()) + : r.getString(); + world.generatorVersion = std::to_string(r.getUint64()); + } + if (world.version >= 181) { + for (int i = 0; i < 16; ++i) { + if (i == 4 || i == 6 || i == 8 || i == 10) { + world.guid += '-'; + } + world.guid += std::format("{:x}", r.getUint8()); + } + } + world.id = r.getUint32(); + world.left = r.getUint32(); + world.right = r.getUint32(); + world.top = r.getUint32(); + world.bottom = r.getUint32(); + world.height = r.getUint32(); + world.width = r.getUint32(); + if (world.version >= 209) { + world.gameMode = r.getUint32(); + if (world.version >= 222) { + world.drunkWorld = r.getBool(); + } + if (world.version >= 227) { + world.forTheWorthy = r.getBool(); + } + if (world.version >= 238) { + world.celebrationmk10 = r.getBool(); + } + if (world.version >= 239) { + world.theConstant = r.getBool(); + } + if (world.version >= 241) { + world.notTheBees = r.getBool(); + } + if (world.version >= 249) { + world.dontDigUp = r.getBool(); + } + if (world.version >= 266) { + world.noTraps = r.getBool(); + } + world.getFixedBoi = world.version < 267 + ? world.drunkWorld && world.dontDigUp + : r.getBool(); + } else if (world.version >= 112) { + world.gameMode = r.getBool() ? 1 : 0; + } + if (world.version >= 141) { + world.creationTime = parseBinaryTime(r.getUint64()); + } + + world.moonType = r.getUint8(); + for (auto &val : world.treeStyleCoords) { + val = r.getUint32(); + } + for (auto &val : world.treeStyles) { + val = r.getUint32(); + } + for (auto &val : world.caveStyleCoords) { + val = r.getUint32(); + } + for (auto &val : world.caveStyles) { + val = r.getUint32(); + } + world.iceStyle = r.getUint32(); + world.jungleStyle = r.getUint32(); + world.underworldStyle = r.getUint32(); + for (auto &val : world.spawn) { + val = r.getUint32(); + } + world.undergroundLevel = r.getFloat64(); + world.cavernLevel = r.getFloat64(); + world.gameTime = r.getFloat64(); + world.isDay = r.getBool(); + world.moonPhase = r.getUint32(); + world.bloodMoon = r.getBool(); + world.eclipse = r.getBool(); + for (auto &val : world.dungeon) { + val = r.getUint32(); + } + world.isCrimson = r.getBool(); + + world.downedEyeOfCthulu = r.getBool(); + world.downedEaterOfWorlds = r.getBool(); + world.downedSkeletron = r.getBool(); + world.downedQueenBee = r.getBool(); + world.downedTheDestroyer = r.getBool(); + world.downedTheTwins = r.getBool(); + world.downedSkeletronPrime = r.getBool(); + world.downedAnyHardmodeBoss = r.getBool(); + world.downedPlantera = r.getBool(); + world.downedGolem = r.getBool(); + if (world.version >= 118) { + world.downedSlimeKing = r.getBool(); + } + world.savedGoblinTinkerer = r.getBool(); + world.savedWizard = r.getBool(); + world.savedMechanic = r.getBool(); + world.defeatedGoblinInvasion = r.getBool(); + world.downedClown = r.getBool(); + world.defeatedFrostLegion = r.getBool(); + world.defeatedPirates = r.getBool(); + + world.brokeAShadowOrb = r.getBool(); + world.meteorSpawned = r.getBool(); + world.shadowOrbsBroken = r.getUint8(); + world.altarsSmashed = r.getUint32(); + world.hardMode = r.getBool(); + if (world.version >= 257) { + world.partyOfDoom = r.getBool(); + } + world.goblinInvasionDelay = r.getUint32(); + world.goblinInvasionSize = r.getUint32(); + world.goblinInvasionType = r.getUint32(); + world.goblinInvasionX = r.getFloat64(); + if (world.version >= 118) { + world.slimeRainTime = r.getFloat64(); + } + if (world.version >= 113) { + world.sundialCooldown = r.getUint8(); + } + world.raining = r.getBool(); + world.rainTimeLeft = r.getUint32(); + world.maxRain = r.getFloat32(); + world.cobaltVariant = r.getUint32(); + world.mythrilVariant = r.getUint32(); + world.adamantiteVariant = r.getUint32(); + world.forestStyle1 = r.getUint8(); + world.corruptionStyle = r.getUint8(); + world.undergroundJungleStyle = r.getUint8(); + world.snowStyle = r.getUint8(); + world.hallowStyle = r.getUint8(); + world.crimsonStyle = r.getUint8(); + world.desertStyle = r.getUint8(); + world.oceanStyle = r.getUint8(); + world.cloudBackground = r.getUint32(); + world.numberOfClouds = r.getUint16(); + world.windSpeed = r.getFloat32(); + if (world.version < 95) { + return; + } + + for (int i = r.getUint32(); i > 0; --i) { + world.anglersFinishedDailyQuest.push_back(r.getString()); + } + world.savedAngler = r.getBool(); + if (world.version < 101) { + return; + } + world.anglerQuest = r.getUint32(); + if (world.version < 104) { + return; + } + world.savedStylist = r.getBool(); + world.savedTaxCollector = r.getBool(); + if (world.version >= 201) { + world.savedGolfer = r.getBool(); + } + world.invasionStartSize = r.getUint32(); + world.cultistDelay = r.getUint32(); + for (int i = r.getUint16(); i > 0; --i) { + world.enemyKillTallies.push_back(r.getUint32()); + } + world.fastForwardTimeToDawn = r.getBool(); + + world.downedFishron = r.getBool(); + world.downedMartians = r.getBool(); + world.downedLunaticCultist = r.getBool(); + world.downedMoonlord = r.getBool(); + world.downedHalloweenPumpking = r.getBool(); + world.downedHalloweenMourningWood = r.getBool(); + world.downedChristmasIceQueen = r.getBool(); + world.downedChristmasSantaNK1 = r.getBool(); + world.downedChristmasEverscream = r.getBool(); + world.downedTowerSolar = r.getBool(); + world.downedTowerVortex = r.getBool(); + world.downedTowerNebula = r.getBool(); + world.downedTowerStardust = r.getBool(); + world.towerActiveSolar = r.getBool(); + world.towerActiveVortex = r.getBool(); + world.towerActiveNebula = r.getBool(); + world.towerActiveStardust = r.getBool(); + world.lunarApocalypseIsUp = r.getBool(); + + if (world.version >= 170) { + world.partyManual = r.getBool(); + world.partyGenuine = r.getBool(); + world.partyCooldown = r.getUint32(); + for (int i = r.getUint32(); i > 0; --i) { + world.partyingNPCs.push_back(r.getUint32()); + } + } + if (world.version >= 174) { + world.sandstormActive = r.getBool(); + world.sandstormTimeLeft = r.getUint32(); + world.sandstormSeverity = r.getFloat32(); + world.sandstormIntendedSeverity = r.getFloat32(); + } + if (world.version >= 178) { + world.savedBartender = r.getBool(); + world.downedInvasionTier1 = r.getBool(); + world.downedInvasionTier2 = r.getBool(); + world.downedInvasionTier3 = r.getBool(); + } + if (world.version < 225) { + return; + } + world.mushroomStyle = r.getUint8(); + world.underworldStyle2 = r.getUint8(); + world.forestStyle2 = r.getUint8(); + world.forestStyle3 = r.getUint8(); + world.forestStyle4 = r.getUint8(); + world.combatBookUsed = r.getBool(); + world.lanternNightCooldown = r.getUint32(); + world.lanternNightGenuine = r.getBool(); + world.lanternNightManual = r.getBool(); + world.lanternNightNextIsGenuine = r.getBool(); + for (int i = r.getUint32(); i > 0; --i) { + world.treeTopVariations.push_back(r.getUint32()); + } + world.forceHalloween = r.getBool(); + world.forceChristmas = r.getBool(); + world.copperVariant = r.getUint32(); + world.ironVariant = r.getUint32(); + world.silverVariant = r.getUint32(); + world.goldVariant = r.getUint32(); + world.boughtCat = r.getBool(); + world.boughtDog = r.getBool(); + world.boughtBunny = r.getBool(); + + if (world.version >= 223) { + world.downedEmpressOfLight = r.getBool(); + world.downedQueenSlime = r.getBool(); + } + if (world.version >= 240) { + world.downedDeerclops = r.getBool(); + } + if (world.version < 269) { + return; + } + world.unlockedSlimeBlue = r.getBool(); + world.unlockedMerchant = r.getBool(); + world.unlockedDemolitionist = r.getBool(); + world.unlockedPartyGirl = r.getBool(); + world.unlockedDyeTrader = r.getBool(); + world.unlockedTruffle = r.getBool(); + world.unlockedArmsDealer = r.getBool(); + world.unlockedNurse = r.getBool(); + world.unlockedPrincess = r.getBool(); + world.combatBookVolumeTwoUsed = r.getBool(); + world.peddlersSatchelUsed = r.getBool(); + world.unlockedSlimeGreen = r.getBool(); + world.unlockedSlimeOld = r.getBool(); + world.unlockedSlimePurple = r.getBool(); + world.unlockedSlimeRainbow = r.getBool(); + world.unlockedSlimeRed = r.getBool(); + world.unlockedSlimeYellow = r.getBool(); + world.unlockedSlimeCopper = r.getBool(); + world.fastForwardTimeToDusk = r.getBool(); + world.moondialCooldown = r.getUint8(); +} + +void readWorldFile(const std::string &data) +{ + Reader r{data}; + World world{}; + readProperties(r, world); + +#ifndef __EMSCRIPTEN__ + printWorld(world); +#endif +} + +#ifndef __EMSCRIPTEN__ +#include +#include +#include + +int main() +{ + std::ifstream in("/path/to/Test_World.wld", std::ios::binary); + std::ostringstream sstr; + sstr << in.rdbuf(); + std::string data{sstr.str()}; + readWorldFile(data); +} + +#define DUMP(field) std::cout << #field " " << world.field << '\n' +#define DUMP_ARRAY(field) \ + do { \ + std::cout << #field " ["; \ + for (auto val : world.field) { \ + std::cout << val << ','; \ + }; \ + std::cout << "]\n"; \ + } while (0) + +void printWorld(World &world) +{ + DUMP(version); + DUMP(revision); + DUMP(isFavorite); + DUMP(framedTiles.size()); + + DUMP(name); + DUMP(seed); + DUMP(generatorVersion); + DUMP(guid); + DUMP(id); + DUMP(left); + DUMP(right); + DUMP(top); + DUMP(bottom); + DUMP(height); + DUMP(width); + DUMP(gameMode); + DUMP(drunkWorld); + DUMP(forTheWorthy); + DUMP(celebrationmk10); + DUMP(theConstant); + DUMP(notTheBees); + DUMP(dontDigUp); + DUMP(noTraps); + DUMP(getFixedBoi); + DUMP(creationTime); + + DUMP(moonType); + DUMP_ARRAY(treeStyleCoords); + DUMP_ARRAY(treeStyles); + DUMP_ARRAY(caveStyleCoords); + DUMP_ARRAY(caveStyles); + DUMP(iceStyle); + DUMP(jungleStyle); + DUMP(underworldStyle); + DUMP_ARRAY(spawn); + DUMP(undergroundLevel); + DUMP(cavernLevel); + DUMP(gameTime); + DUMP(isDay); + DUMP(moonPhase); + DUMP(bloodMoon); + DUMP(eclipse); + DUMP_ARRAY(dungeon); + DUMP(isCrimson); + + DUMP(downedEyeOfCthulu); + DUMP(downedEaterOfWorlds); + DUMP(downedSkeletron); + DUMP(downedQueenBee); + DUMP(downedTheDestroyer); + DUMP(downedTheTwins); + DUMP(downedSkeletronPrime); + DUMP(downedAnyHardmodeBoss); + DUMP(downedPlantera); + DUMP(downedGolem); + DUMP(downedSlimeKing); + DUMP(savedGoblinTinkerer); + DUMP(savedWizard); + DUMP(savedMechanic); + DUMP(defeatedGoblinInvasion); + DUMP(downedClown); + DUMP(defeatedFrostLegion); + DUMP(defeatedPirates); + + DUMP(brokeAShadowOrb); + DUMP(meteorSpawned); + DUMP(shadowOrbsBroken); + DUMP(altarsSmashed); + DUMP(hardMode); + DUMP(partyOfDoom); + DUMP(goblinInvasionDelay); + DUMP(goblinInvasionSize); + DUMP(goblinInvasionType); + DUMP(goblinInvasionX); + DUMP(slimeRainTime); + DUMP(sundialCooldown); + DUMP(raining); + DUMP(rainTimeLeft); + DUMP(maxRain); + DUMP(cobaltVariant); + DUMP(mythrilVariant); + DUMP(adamantiteVariant); + DUMP(forestStyle1); + DUMP(corruptionStyle); + DUMP(undergroundJungleStyle); + DUMP(snowStyle); + DUMP(hallowStyle); + DUMP(crimsonStyle); + DUMP(desertStyle); + DUMP(oceanStyle); + DUMP(cloudBackground); + DUMP(numberOfClouds); + DUMP(windSpeed); + + DUMP_ARRAY(anglersFinishedDailyQuest); + DUMP(savedAngler); + DUMP(anglerQuest); + DUMP(savedStylist); + DUMP(savedTaxCollector); + DUMP(savedGolfer); + DUMP(invasionStartSize); + DUMP(cultistDelay); + DUMP(enemyKillTallies.size()); + DUMP(fastForwardTimeToDawn); + + DUMP(downedFishron); + DUMP(downedMartians); + DUMP(downedLunaticCultist); + DUMP(downedMoonlord); + DUMP(downedHalloweenPumpking); + DUMP(downedHalloweenMourningWood); + DUMP(downedChristmasIceQueen); + DUMP(downedChristmasSantaNK1); + DUMP(downedChristmasEverscream); + DUMP(downedTowerSolar); + DUMP(downedTowerVortex); + DUMP(downedTowerNebula); + DUMP(downedTowerStardust); + DUMP(towerActiveSolar); + DUMP(towerActiveVortex); + DUMP(towerActiveNebula); + DUMP(towerActiveStardust); + DUMP(lunarApocalypseIsUp); + + DUMP(partyManual); + DUMP(partyGenuine); + DUMP(partyCooldown); + DUMP_ARRAY(partyingNPCs); + DUMP(sandstormActive); + DUMP(sandstormTimeLeft); + DUMP(sandstormSeverity); + DUMP(sandstormIntendedSeverity); + DUMP(savedBartender); + DUMP(downedInvasionTier1); + DUMP(downedInvasionTier2); + DUMP(downedInvasionTier3); + DUMP(mushroomStyle); + DUMP(underworldStyle2); + DUMP(forestStyle2); + DUMP(forestStyle3); + DUMP(forestStyle4); + DUMP(combatBookUsed); + DUMP(lanternNightCooldown); + DUMP(lanternNightGenuine); + DUMP(lanternNightManual); + DUMP(lanternNightNextIsGenuine); + DUMP_ARRAY(treeTopVariations); + DUMP(forceHalloween); + DUMP(forceChristmas); + DUMP(copperVariant); + DUMP(ironVariant); + DUMP(silverVariant); + DUMP(goldVariant); + DUMP(boughtCat); + DUMP(boughtDog); + DUMP(boughtBunny); + + DUMP(downedEmpressOfLight); + DUMP(downedQueenSlime); + DUMP(downedDeerclops); + DUMP(unlockedSlimeBlue); + DUMP(unlockedMerchant); + DUMP(unlockedDemolitionist); + DUMP(unlockedPartyGirl); + DUMP(unlockedDyeTrader); + DUMP(unlockedTruffle); + DUMP(unlockedArmsDealer); + DUMP(unlockedNurse); + DUMP(unlockedPrincess); + DUMP(combatBookVolumeTwoUsed); + DUMP(peddlersSatchelUsed); + DUMP(unlockedSlimeGreen); + DUMP(unlockedSlimeOld); + DUMP(unlockedSlimePurple); + DUMP(unlockedSlimeRainbow); + DUMP(unlockedSlimeRed); + DUMP(unlockedSlimeYellow); + DUMP(unlockedSlimeCopper); + DUMP(fastForwardTimeToDusk); + DUMP(moondialCooldown); +} +#endif diff --git a/wasm/src/ieee754_types.hpp b/wasm/src/ieee754_types.hpp new file mode 100644 index 0000000..a47ed35 --- /dev/null +++ b/wasm/src/ieee754_types.hpp @@ -0,0 +1,169 @@ +// https://github.com/kkimdev/ieee754-types +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef IEEE754_TYPES_HPP_ +#define IEEE754_TYPES_HPP_ + +// Based on IEEE 754-2008 + +#if __cplusplus < 201703L +#error This file requires C++17 +#endif + +#include +#include +#include + +namespace IEEE_754 { +namespace detail { + +template +inline constexpr int get_storage_bits() { + return sizeof(T) * CHAR_BIT; +} + +template +inline constexpr int get_exponent_bits() { + int exponent_range = ::std::numeric_limits::max_exponent - + ::std::numeric_limits::min_exponent; + int bits = 0; + while ((exponent_range >> bits) > 0) ++bits; + return bits; +} + +template +inline constexpr int get_mantissa_bits() { + return ::std::numeric_limits::digits - 1; +} + +template +inline constexpr int standard_binary_interchange_format_exponent_bits() { + constexpr bool is_valid_storage_bits = + storage_bits == 16 || // + storage_bits == 32 || // + storage_bits == 64 || // + storage_bits == 128 || // + (storage_bits > 128 && storage_bits % 32 == 0); + static_assert( + is_valid_storage_bits, + "IEEE 754-2008 standard binary interchange formats are only defined for " + "the following storage width in bits: 16, 32, 64, 128, and any multiple " + "of 32 of at least 128."); + static_assert(!(is_valid_storage_bits && storage_bits > 128), + "Not Implemented for storage bits larger than 128."); + + if (storage_bits == 16) return 5; + if (storage_bits == 32) return 8; + if (storage_bits == 64) return 11; + if (storage_bits == 128) return 15; + + throw; +} + +template +inline constexpr int standard_binary_interchange_format_mantissa_bits() { + return storage_bits - + standard_binary_interchange_format_exponent_bits() - 1; +} + +static_assert(standard_binary_interchange_format_exponent_bits<16>() == 5, ""); +static_assert(standard_binary_interchange_format_exponent_bits<32>() == 8, ""); +static_assert(standard_binary_interchange_format_exponent_bits<64>() == 11, ""); +static_assert(standard_binary_interchange_format_exponent_bits<128>() == 15, ""); + +static_assert(standard_binary_interchange_format_mantissa_bits<16>() == 10, ""); +static_assert(standard_binary_interchange_format_mantissa_bits<32>() == 23, ""); +static_assert(standard_binary_interchange_format_mantissa_bits<64>() == 52, ""); +static_assert(standard_binary_interchange_format_mantissa_bits<128>() == 112,""); + +template +struct Is_Ieee754_2008_Binary_Interchange_Format { + // TODO: as of 2018-06-11 clang-format doesn't handle the following section + // well. + // clang-format off + template + static constexpr bool value = + ::std::is_floating_point() && + ::std::numeric_limits::is_iec559 && + ::std::numeric_limits::radix == 2 && + get_storage_bits() == storage_bits && + get_exponent_bits() == exponent_bits && + get_mantissa_bits() == mantissa_bits; + // clang-format on +}; + +template +inline constexpr auto find_type() { + throw; + + if constexpr (C::template value) { + return T(); + } else if constexpr (sizeof...(Ts) >= 1) { + return find_type(); + } else { + return void(); + } +} + +template (), + int mantissa_bits = + standard_binary_interchange_format_mantissa_bits()> +using BinaryFloatOrVoid = + decltype(find_type< // + Is_Ieee754_2008_Binary_Interchange_Format, + float, double, long double>()); + +template +struct AssertTypeFound { + static_assert( + !::std::is_same_v, + "No corresponding IEEE 754-2008 binary interchange format found."); + using type = T; +}; + +} // namespace detail + +namespace _2008 { +template +using Binary = typename detail::AssertTypeFound< + detail::BinaryFloatOrVoid>::type; +} // namespace _2008 + +// Testing +namespace detail { + +template +inline void test_if_type_exists() { + throw; + + if constexpr (!::std::is_same_v, void>) { + using T = ::IEEE_754::_2008::Binary; + static_assert(::std::is_floating_point(), ""); + static_assert(::std::numeric_limits::is_iec559, ""); + static_assert(::std::numeric_limits::radix == 2, ""); + static_assert(get_storage_bits() == storage_bits, ""); + static_assert(get_exponent_bits() == exponent_bits, ""); + static_assert(get_mantissa_bits() == mantissa_bits, ""); + } +} + +inline void tests() { + throw; + + test_if_type_exists<16, 5, 10>(); + test_if_type_exists<32, 8, 23>(); + test_if_type_exists<64, 11, 52>(); + test_if_type_exists<128, 15, 112>(); +} + +} // namespace detail + +} // namespace IEEE_754 + +#endif // IEEE754_TYPES_HPP_ From 8462187901a77e0810e19d9fb9a4242459324be4 Mon Sep 17 00:00:00 2001 From: Alpha Date: Tue, 5 Aug 2025 19:54:46 -0400 Subject: [PATCH 02/13] Wasm: implement tile reader --- resources/js/main.js | 16 +- wasm/src/Makefile | 22 ++- wasm/src/Tile.h | 32 ++++ wasm/src/TileColor.cpp | 336 +++++++++++++++++++++++++++++++++++++++ wasm/src/TileColor.h | 25 +++ wasm/src/World.cpp | 12 ++ wasm/src/World.h | 6 + wasm/src/WorldLoader.cpp | 312 +++++++++++------------------------- wasm/src/WorldLoader.h | 8 + wasm/src/header.js | 1 + wasm/src/main.cpp | 249 +++++++++++++++++++++++++++++ wasm/src/pre.js | 8 + wasm/src/worker.js | 16 ++ 13 files changed, 814 insertions(+), 229 deletions(-) create mode 100644 wasm/src/Tile.h create mode 100644 wasm/src/TileColor.cpp create mode 100644 wasm/src/TileColor.h create mode 100644 wasm/src/World.cpp create mode 100644 wasm/src/WorldLoader.h create mode 100644 wasm/src/header.js create mode 100644 wasm/src/main.cpp create mode 100644 wasm/src/pre.js create mode 100644 wasm/src/worker.js diff --git a/resources/js/main.js b/resources/js/main.js index 37e01d4..df2f259 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -33,6 +33,8 @@ var file; var world; +var worker = null; + var selectionX = 0; var selectionY = 0; @@ -864,8 +866,10 @@ function fileNameChanged (evt) { } function reloadWorld() { - var worker = new Worker('resources/js/WorldLoader.js'); - worker.addEventListener('message', onWorldLoaderWorkerMessage); + if (worker === null) { + worker = new Worker('wasm/src/build/terramap.js'); + worker.addEventListener('message', onWorldLoaderWorkerMessage); + } worker.postMessage(file); } @@ -973,10 +977,14 @@ function onWorldLoaderWorkerMessage(e) { Object.keys(world).filter(key => { const value = world[key]; const type = typeof value; - return type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint'; + return type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint' || ( + Array.isArray(value) && value.length < 5 + ); }).sort() .forEach(key => - $("#worldPropertyList").append(`
  • ${key}: ${world[key]}
  • `) + $("#worldPropertyList").append( + `
  • ${key}: ${Array.isArray(world[key]) ? JSON.stringify(world[key]) : world[key]}
  • ` + ) ); } } diff --git a/wasm/src/Makefile b/wasm/src/Makefile index b8eb130..6b7ec41 100644 --- a/wasm/src/Makefile +++ b/wasm/src/Makefile @@ -1,9 +1,16 @@ # Config values CXXFLAGS := -Wall -Wextra -pedantic -Werror -std=c++20 -O2 -LDFLAGS := -O2 - -SRCS := Reader.cpp WorldLoader.cpp +LDFLAGS := -lembind \ + -sENVIRONMENT=worker \ + -sALLOW_MEMORY_GROWTH=1 \ + -sSTACK_SIZE=16MB \ + --extern-pre-js header.js \ + --pre-js pre.js \ + --extern-post-js worker.js \ + -O2 --closure 1 + +SRCS := main.cpp Reader.cpp TileColor.cpp World.cpp WorldLoader.cpp OUT := terramap BUILD_DIR := build @@ -15,17 +22,22 @@ CPPFLAGS := -Isrc OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) -$(BUILD_DIR)/$(OUT): $(OBJS) +$(BUILD_DIR)/$(OUT).js: $(OBJS) header.js pre.js worker.js $(CXX) $(OBJS) -o $@ $(LDFLAGS) $(BUILD_DIR)/%.cpp.o: %.cpp @mkdir -p $(dir $@) $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ +$(BUILD_DIR)/$(OUT): $(OBJS) + $(CXX) $(OBJS) -o $@ + +cli: $(BUILD_DIR)/$(OUT) + clean: rm -r $(BUILD_DIR) format: clang-format -i $(SRCS) -.PHONY: clean format +.PHONY: cli clean format diff --git a/wasm/src/Tile.h b/wasm/src/Tile.h new file mode 100644 index 0000000..d97d3ec --- /dev/null +++ b/wasm/src/Tile.h @@ -0,0 +1,32 @@ +#ifndef TILE_H +#define TILE_H + +enum class Liquid { none, water, lava, honey, shimmer }; + +enum class Slope { none = 0, half, topRight, topLeft, bottomRight, bottomLeft }; + +class Tile +{ +public: + int blockId; + int frameX; + int frameY; + int wallId; + int blockPaint; + int wallPaint; + int liquidAmount; + Liquid liquid; + Slope slope; + bool wireRed : 1; + bool wireBlue : 1; + bool wireGreen : 1; + bool wireYellow : 1; + bool actuated : 1; + bool actuator : 1; + bool echoCoatBlock : 1; + bool echoCoatWall : 1; + bool illuminantBlock : 1; + bool illuminantWall : 1; +}; + +#endif // TILE_H diff --git a/wasm/src/TileColor.cpp b/wasm/src/TileColor.cpp new file mode 100644 index 0000000..dd2f29e --- /dev/null +++ b/wasm/src/TileColor.cpp @@ -0,0 +1,336 @@ +#include "TileColor.h" + +#include "World.h" +#include + +namespace +{ +// clang-format off +uint8_t blockColors[] = { + 151, 107, 75, 128, 128, 128, 28, 216, 94, 26, 196, 84, 253, 221, 3, 151, + 107, 75, 140, 101, 80, 150, 67, 22, 185, 164, 23, 185, 194, 195, 119, 105, + 79, 119, 105, 79, 174, 24, 69, 133, 213, 247, 191, 142, 111, 191, 142, 111, + 140, 130, 116, 144, 148, 144, 191, 142, 111, 191, 142, 111, 163, 116, 81, + 174, 129, 92, 98, 95, 167, 141, 137, 223, 122, 116, 218, 109, 90, 128, 119, + 101, 125, 54, 154, 54, 151, 79, 80, 175, 105, 128, 151, 107, 75, 141, 120, + 168, 151, 135, 183, 253, 221, 3, 235, 166, 135, 226, 145, 30, 230, 89, 92, + 104, 86, 84, 128, 128, 128, 181, 62, 59, 146, 81, 68, 66, 84, 109, 251, + 235, 127, 84, 100, 63, 107, 68, 99, 185, 164, 23, 185, 194, 195, 150, 67, + 22, 128, 128, 128, 89, 201, 255, 170, 48, 114, 192, 202, 203, 23, 177, 76, + 186, 168, 84, 200, 246, 254, 191, 142, 111, 43, 40, 84, 68, 68, 76, 142, + 66, 66, 92, 68, 73, 143, 215, 29, 135, 196, 26, 121, 176, 24, 110, 140, + 182, 196, 96, 114, 56, 150, 97, 160, 118, 58, 140, 58, 166, 125, 191, 197, + 190, 150, 92, 93, 127, 255, 182, 175, 130, 182, 175, 130, 27, 197, 109, 96, + 197, 27, 26, 26, 26, 142, 66, 66, 238, 85, 70, 121, 110, 97, 191, 142, 111, + 73, 120, 17, 245, 133, 191, 246, 197, 26, 246, 197, 26, 246, 197, 26, 192, + 192, 192, 191, 142, 111, 191, 142, 111, 191, 142, 111, 191, 142, 111, 144, + 148, 144, 13, 88, 130, 213, 229, 237, 253, 221, 3, 191, 142, 111, 255, 162, + 31, 144, 148, 144, 144, 148, 144, 253, 221, 3, 144, 148, 144, 253, 221, 3, + 191, 142, 111, 229, 212, 73, 141, 98, 77, 191, 142, 111, 144, 148, 144, + 191, 142, 111, 11, 80, 143, 91, 169, 169, 78, 193, 227, 48, 186, 135, 128, + 26, 52, 103, 98, 122, 48, 208, 234, 191, 142, 111, 33, 171, 207, 238, 225, + 218, 181, 172, 190, 238, 225, 218, 107, 92, 108, 92, 68, 73, 11, 80, 143, + 91, 169, 169, 106, 107, 118, 73, 51, 36, 141, 175, 255, 159, 209, 229, 128, + 204, 230, 191, 142, 111, 255, 117, 224, 128, 128, 128, 52, 52, 52, 144, + 148, 144, 231, 53, 56, 166, 187, 153, 253, 114, 114, 213, 203, 204, 144, + 148, 144, 128, 128, 128, 191, 142, 111, 98, 95, 167, 192, 59, 59, 144, 148, + 144, 144, 148, 144, 144, 148, 144, 192, 30, 30, 43, 192, 30, 211, 236, 241, + 211, 236, 241, 220, 50, 50, 128, 26, 52, 190, 171, 94, 128, 133, 184, 239, + 141, 126, 190, 171, 94, 131, 162, 161, 170, 171, 157, 104, 100, 126, 145, + 81, 85, 148, 133, 98, 255, 76, 76, 144, 195, 232, 184, 219, 240, 174, 145, + 214, 218, 182, 204, 115, 173, 229, 129, 125, 93, 62, 82, 114, 132, 157, + 127, 152, 171, 198, 27, 109, 69, 33, 135, 85, 191, 142, 111, 253, 221, 3, + 253, 221, 3, 129, 125, 93, 132, 157, 127, 152, 171, 198, 208, 94, 201, 49, + 134, 114, 126, 134, 49, 134, 59, 49, 43, 86, 140, 121, 49, 134, 29, 106, + 88, 99, 99, 99, 99, 99, 99, 99, 99, 99, 73, 120, 17, 223, 255, 255, 182, + 175, 130, 151, 107, 75, 26, 196, 84, 56, 121, 255, 157, 157, 107, 134, 22, + 34, 147, 144, 178, 97, 200, 225, 62, 61, 52, 208, 80, 80, 216, 152, 144, + 203, 61, 64, 213, 178, 28, 128, 44, 45, 125, 55, 65, 186, 50, 52, 124, 175, + 201, 144, 148, 144, 88, 105, 118, 144, 148, 144, 192, 59, 59, 191, 233, + 115, 144, 148, 144, 137, 120, 67, 103, 103, 103, 254, 121, 2, 191, 142, + 111, 144, 148, 144, 144, 148, 144, 144, 148, 144, 144, 148, 144, 239, 90, + 50, 231, 96, 228, 57, 85, 101, 107, 132, 139, 227, 125, 22, 141, 56, 0, 74, + 197, 155, 144, 148, 144, 255, 156, 12, 131, 79, 13, 224, 194, 101, 145, 81, + 85, 107, 182, 29, 53, 44, 41, 214, 184, 46, 149, 232, 87, 255, 241, 51, + 225, 128, 206, 224, 194, 101, 120, 85, 60, 77, 74, 72, 99, 50, 30, 198, + 196, 170, 200, 245, 253, 99, 50, 30, 99, 50, 30, 140, 150, 150, 219, 71, + 38, 235, 38, 231, 86, 85, 92, 235, 150, 23, 153, 131, 44, 57, 48, 97, 248, + 158, 92, 107, 49, 154, 154, 148, 49, 49, 49, 154, 49, 154, 68, 154, 49, 77, + 85, 89, 118, 154, 83, 49, 221, 79, 255, 250, 255, 79, 79, 102, 255, 79, + 255, 89, 255, 79, 79, 240, 240, 247, 255, 145, 79, 191, 142, 111, 187, 255, + 107, 107, 250, 255, 121, 119, 101, 128, 128, 128, 190, 171, 94, 122, 217, + 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, + 232, 122, 217, 232, 122, 217, 232, 128, 128, 128, 150, 67, 22, 122, 217, + 232, 122, 217, 232, 79, 128, 17, 122, 217, 232, 122, 217, 232, 122, 217, + 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, + 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 144, 148, + 144, 144, 148, 144, 144, 148, 144, 144, 148, 144, 144, 148, 144, 144, 148, + 144, 144, 148, 144, 144, 148, 144, 144, 148, 144, 122, 217, 232, 122, 217, + 232, 117, 61, 25, 204, 93, 73, 87, 150, 154, 181, 164, 125, 235, 114, 80, + 157, 176, 226, 118, 227, 129, 227, 118, 215, 96, 68, 48, 203, 185, 151, 96, + 77, 64, 198, 170, 104, 182, 141, 86, 228, 213, 173, 129, 125, 93, 9, 61, + 191, 253, 32, 3, 200, 246, 254, 15, 15, 15, 226, 118, 76, 161, 172, 173, + 204, 181, 72, 190, 190, 178, 191, 142, 111, 217, 174, 137, 253, 62, 3, 144, + 148, 144, 85, 255, 160, 122, 217, 232, 96, 248, 2, 105, 74, 202, 29, 240, + 255, 254, 202, 80, 131, 252, 245, 255, 156, 12, 149, 212, 89, 236, 74, 79, + 44, 26, 233, 144, 148, 144, 55, 97, 155, 31, 31, 31, 238, 97, 94, 28, 216, + 94, 141, 107, 89, 141, 107, 89, 233, 203, 24, 168, 178, 204, 122, 217, 232, + 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, + 122, 217, 232, 146, 136, 205, 223, 232, 233, 168, 178, 204, 50, 46, 104, + 50, 46, 104, 127, 116, 194, 249, 101, 189, 252, 128, 201, 9, 61, 191, 253, + 32, 3, 255, 156, 12, 160, 120, 92, 191, 142, 111, 160, 120, 100, 251, 209, + 240, 191, 142, 111, 254, 121, 2, 28, 216, 94, 221, 136, 144, 131, 206, 12, + 87, 21, 144, 127, 92, 69, 127, 92, 69, 127, 92, 69, 127, 92, 69, 253, 32, + 3, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 191, 142, + 111, 198, 124, 78, 212, 192, 100, 100, 82, 126, 77, 76, 66, 96, 68, 117, + 68, 60, 51, 174, 168, 186, 205, 152, 186, 212, 148, 88, 140, 140, 140, 120, + 120, 120, 255, 227, 132, 85, 83, 82, 85, 83, 82, 75, 139, 166, 227, 46, 46, + 75, 139, 166, 122, 217, 232, 122, 217, 232, 249, 75, 7, 0, 160, 170, 160, + 87, 234, 22, 173, 254, 88, 95, 114, 99, 255, 107, 65, 75, 90, 65, 75, 90, + 245, 197, 1, 146, 155, 187, 146, 155, 187, 168, 38, 47, 183, 53, 62, 255, + 255, 255, 220, 220, 220, 39, 168, 96, 39, 94, 168, 242, 221, 100, 224, 100, + 242, 197, 193, 216, 54, 183, 111, 54, 109, 183, 255, 236, 115, 239, 115, + 255, 212, 208, 231, 238, 51, 53, 174, 129, 92, 3, 144, 201, 144, 148, 144, + 191, 176, 124, 240, 240, 240, 255, 66, 152, 179, 132, 255, 0, 206, 180, 91, + 186, 240, 92, 240, 91, 240, 91, 147, 255, 150, 181, 179, 132, 255, 174, 16, + 176, 48, 225, 110, 179, 132, 255, 150, 164, 206, 211, 198, 111, 190, 223, + 232, 141, 163, 181, 212, 192, 100, 231, 178, 28, 155, 214, 240, 233, 183, + 128, 51, 84, 195, 205, 153, 73, 129, 56, 121, 129, 56, 121, 191, 142, 111, + 191, 142, 111, 191, 142, 111, 190, 160, 140, 85, 114, 123, 116, 94, 97, + 191, 142, 111, 160, 160, 160, 28, 216, 94, 108, 34, 35, 178, 114, 68, 120, + 50, 50, 66, 84, 109, 84, 100, 63, 107, 68, 99, 73, 120, 17, 198, 134, 88, + 191, 142, 111, 191, 142, 111, 127, 92, 69, 255, 29, 136, 211, 211, 211, 60, + 20, 160, 78, 193, 227, 250, 249, 252, 224, 219, 236, 253, 227, 215, 165, + 159, 153, 191, 142, 111, 202, 174, 165, 160, 187, 142, 254, 158, 35, 34, + 221, 151, 249, 170, 236, 35, 200, 254, 92, 75, 118, 122, 217, 232, 61, 61, + 61, 5, 5, 5, 5, 5, 5, 50, 50, 60, 191, 142, 111, 187, 68, 74, 49, 134, 114, + 126, 134, 49, 134, 59, 49, 43, 86, 140, 121, 49, 134, 254, 121, 2, 26, 196, + 84, 28, 216, 109, 224, 219, 236, 122, 217, 232, 122, 217, 232, 122, 217, + 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 182, 175, + 130, 99, 150, 8, 107, 182, 0, 144, 148, 144, 122, 217, 232, 122, 217, 232, + 114, 254, 2, 114, 254, 2, 0, 197, 208, 0, 197, 208, 122, 217, 232, 208, 0, + 126, 208, 0, 126, 50, 107, 197, 122, 217, 232, 122, 217, 232, 122, 217, + 232, 255, 126, 145, 60, 60, 60, 120, 110, 100, 120, 110, 100, 54, 83, 20, + 122, 217, 232, 122, 217, 232, 186, 168, 84, 122, 217, 232, 122, 217, 232, + 122, 217, 232, 122, 217, 232, 60, 60, 60, 122, 217, 232, 122, 217, 232, + 150, 67, 22, 148, 158, 184, 165, 168, 26, 165, 168, 26, 87, 127, 220, 99, + 99, 99, 233, 180, 90, 144, 148, 144, 248, 203, 233, 203, 248, 218, 160, + 242, 255, 165, 168, 26, 255, 186, 212, 191, 142, 111, 76, 57, 44, 125, 61, + 65, 30, 26, 84, 178, 104, 58, 172, 155, 110, 99, 99, 99, 122, 217, 232, + 255, 150, 150, 122, 217, 232, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 114, 81, 56, 254, 121, 2, 119, 105, 79, 119, 105, 79, 151, 107, 75, 151, + 107, 75, 28, 216, 94, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, + 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, + 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, 217, 232, 122, + 217, 232, 122, 217, 232, 250, 100, 50, 250, 100, 50, 151, 107, 75, 151, + 107, 75, 233, 207, 94, 128, 128, 128, 122, 217, 232, 122, 217, 232, 250, + 250, 250, 235, 235, 249, 220, 210, 245, 210, 91, 77, 220, 12, 237, 220, 12, + 237, 255, 76, 76, 255, 76, 76, 122, 217, 232, 117, 145, 73, 122, 234, 225, + 122, 217, 232, 210, 140, 100, 145, 120, 120, 145, 120, 120, 122, 116, 218, + 200, 120, 75, 200, 120, 75, 110, 105, 255, 122, 217, 232, 235, 125, 150, + 149, 212, 89, 122, 217, 232, 122, 217, 232, 122, 217, 232, 108, 133, 140, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 107, 182, 29, 107, 182, 29, + 151, 79, 80, 200, 44, 28, 225, 128, 206, 21, 124, 212, 35, 205, 215, 200, + 105, 230, 247, 228, 254, 255, 150, 150, 141, 137, 223, 208, 80, 80, 24, + 203, 233, 128, 128, 128, 174, 24, 69, 115, 60, 40, 247, 228, 254, 151, 107, + 75, 83, 46, 57, 91, 87, 167, 23, 33, 81, 53, 133, 103, 11, 67, 80, 40, 49, + 60, 21, 13, 77, 195, 201, 215, 66, 84, 109, 84, 100, 63, 107, 68, 99, 185, + 164, 23, 185, 194, 195, 150, 67, 22, 100, 90, 190, 142, 66, 66, 11, 80, + 143, 91, 169, 169, 254, 121, 2, 208, 0, 126, 114, 254, 2, 0, 197, 208, 220, + 12, 237, 255, 76, 76 +}; + +uint8_t wallColors[] = { + 0, 0, 0, 52, 52, 52, 88, 61, 46, 61, 58, 78, 73, 51, 36, 52, 52, 52, 91, + 30, 30, 27, 31, 42, 31, 39, 26, 41, 28, 36, 74, 62, 12, 46, 56, 59, 75, 32, + 11, 67, 37, 37, 15, 15, 15, 52, 43, 45, 88, 61, 46, 27, 31, 42, 31, 39, 26, + 41, 28, 36, 15, 15, 15, 54, 89, 98, 113, 99, 99, 38, 38, 43, 53, 39, 41, + 11, 35, 62, 21, 63, 70, 88, 61, 46, 81, 84, 101, 88, 23, 23, 28, 88, 23, + 78, 87, 99, 86, 17, 40, 49, 47, 83, 69, 67, 41, 51, 51, 70, 87, 59, 55, 69, + 67, 41, 49, 57, 49, 78, 79, 73, 85, 102, 103, 52, 50, 62, 71, 42, 44, 73, + 66, 50, 52, 52, 52, 60, 59, 51, 48, 57, 47, 71, 77, 85, 52, 52, 52, 52, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 40, 56, 50, 49, 48, 36, + 43, 33, 32, 31, 40, 49, 48, 35, 52, 88, 61, 46, 1, 52, 20, 55, 39, 26, 39, + 33, 26, 30, 80, 48, 53, 80, 30, 30, 80, 48, 30, 80, 48, 53, 80, 30, 30, 80, + 48, 43, 42, 68, 30, 70, 80, 78, 105, 135, 52, 84, 12, 190, 204, 223, 64, + 62, 80, 65, 65, 35, 20, 46, 104, 61, 13, 16, 63, 39, 26, 51, 47, 96, 64, + 62, 80, 101, 51, 51, 77, 64, 34, 62, 38, 41, 48, 78, 93, 54, 63, 69, 138, + 73, 38, 50, 15, 8, 115, 68, 124, 129, 114, 74, 62, 86, 123, 90, 121, 94, + 135, 62, 61, 100, 96, 103, 32, 40, 45, 44, 41, 50, 72, 50, 77, 78, 50, 69, + 36, 45, 44, 38, 49, 50, 32, 40, 45, 44, 41, 50, 72, 50, 77, 78, 50, 69, 36, + 45, 44, 38, 49, 50, 97, 72, 51, 53, 53, 53, 138, 73, 38, 94, 25, 17, 125, + 36, 122, 51, 35, 27, 50, 15, 8, 135, 58, 0, 65, 52, 15, 39, 42, 51, 89, 26, + 27, 126, 123, 115, 8, 50, 19, 95, 21, 24, 17, 31, 65, 192, 173, 143, 114, + 114, 131, 136, 119, 7, 8, 72, 3, 117, 132, 82, 100, 102, 114, 30, 118, 226, + 93, 6, 102, 64, 40, 169, 39, 34, 180, 87, 94, 125, 6, 6, 6, 69, 72, 186, + 130, 62, 16, 22, 123, 163, 40, 86, 151, 183, 75, 15, 83, 80, 100, 115, 65, + 68, 119, 108, 81, 59, 67, 71, 222, 216, 202, 90, 112, 105, 62, 28, 87, 120, + 120, 120, 120, 59, 19, 59, 59, 59, 229, 218, 161, 73, 59, 50, 81, 69, 62, + 102, 75, 34, 103, 76, 36, 255, 145, 79, 221, 79, 255, 240, 240, 247, 79, + 255, 89, 154, 83, 49, 107, 49, 154, 85, 89, 118, 49, 154, 68, 154, 49, 77, + 49, 49, 154, 154, 148, 49, 255, 79, 79, 79, 102, 255, 250, 255, 79, 70, 68, + 51, 84, 97, 84, 5, 5, 5, 59, 39, 22, 59, 39, 22, 163, 96, 0, 94, 163, 46, + 117, 32, 59, 20, 11, 203, 74, 69, 88, 60, 30, 30, 111, 117, 135, 111, 117, + 135, 25, 23, 54, 25, 23, 54, 74, 71, 129, 111, 117, 135, 25, 23, 54, 52, + 52, 52, 38, 9, 66, 149, 80, 51, 82, 63, 80, 65, 61, 77, 64, 65, 92, 76, 53, + 84, 144, 67, 52, 149, 48, 48, 111, 32, 36, 147, 48, 55, 97, 67, 51, 112, + 80, 62, 88, 61, 46, 127, 94, 76, 143, 50, 123, 136, 120, 131, 219, 92, 143, + 113, 64, 150, 74, 67, 60, 60, 78, 59, 0, 54, 21, 74, 97, 72, 40, 37, 35, + 77, 63, 66, 111, 6, 6, 88, 67, 59, 88, 87, 80, 71, 71, 67, 76, 52, 60, 89, + 48, 59, 158, 100, 64, 62, 45, 75, 57, 14, 12, 96, 72, 133, 67, 55, 80, 64, + 37, 29, 70, 51, 91, 51, 18, 4, 57, 55, 52, 68, 68, 68, 148, 138, 74, 95, + 137, 191, 160, 2, 75, 100, 55, 164, 0, 117, 101, 110, 90, 78, 47, 69, 75, + 91, 67, 70, 60, 36, 39, 140, 75, 48, 127, 49, 44, 200, 44, 18, 24, 93, 66, + 160, 87, 234, 6, 106, 255, 146, 95, 53, 5, 5, 5, 5, 5, 5, 63, 39, 26, 102, + 102, 102, 61, 58, 78, 52, 43, 45, 81, 84, 101, 85, 102, 103, 52, 52, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 40, 56, 50, 49, + 48, 36, 43, 33, 32, 31, 40, 49, 48, 35, 52, 88, 61, 46, 55, 39, 26, 39, 33, + 26, 43, 42, 68, 30, 70, 80, 78, 105, 135, 51, 47, 96, 101, 51, 51, 62, 38, + 41, 59, 39, 22, 59, 39, 22, 111, 117, 135, 25, 23, 54, 52, 52, 52, 149, 80, + 51, 82, 63, 80, 65, 61, 77, 64, 65, 92, 76, 53, 84, 144, 67, 52, 149, 48, + 48, 111, 32, 36, 147, 48, 55, 97, 67, 51, 112, 80, 62, 88, 61, 46, 127, 94, + 76, 143, 50, 123, 136, 120, 131, 219, 92, 143, 113, 64, 150, 74, 67, 60, + 60, 78, 59, 0, 54, 21, 74, 97, 72, 40, 37, 35, 77, 63, 66, 111, 6, 6, 88, + 67, 59, 88, 87, 80, 71, 71, 67, 76, 52, 60, 89, 48, 59, 158, 100, 64, 62, + 45, 75, 57, 14, 12, 96, 72, 133, 67, 55, 80, 64, 37, 29, 70, 51, 91, 51, + 18, 4, 78, 110, 51, 78, 110, 51, 52, 52, 52, 181, 230, 29, 125, 100, 100, + 125, 100, 100, 6, 6, 34, 105, 51, 108, 75, 30, 15, 91, 108, 130, 91, 108, + 130, 55, 25, 33, 60, 55, 145, 10, 5, 50, 30, 105, 75, 5, 45, 55, 20, 25, + 35, 15, 10, 50, 153, 164, 187, 27, 31, 42, 31, 39, 26, 41, 28, 36, 74, 62, + 12, 46, 56, 59, 75, 32, 11, 15, 15, 15, 67, 37, 37, 11, 35, 62, 21, 63, 70, + 100, 40, 1, 92, 30, 72, 42, 81, 1, 1, 81, 109, 56, 22, 97, 52, 52, 52 +}; + +uint8_t paintColors[] = { + 0, 0, 0, 255, 0, 0, 255, 127, 0, 255, 255, 0, 127, 255, 0, 0, 255, 0, 0, + 255, 127, 0, 255, 255, 0, 127, 255, 0, 0, 255, 127, 0, 255, 255, 0, 255, + 255, 0, 127, 255, 0, 0, 255, 127, 0, 255, 255, 0, 127, 255, 0, 0, 255, 0, + 0, 255, 127, 0, 255, 255, 0, 127, 255, 0, 0, 255, 127, 0, 255, 255, 0, 255, + 255, 0, 127, 75, 75, 75, 255, 255, 255, 175, 175, 175, 255, 178, 125, 25, + 25, 25, 255, 255, 255, 255, 255, 255 +}; +// clang-format on + +uint8_t waterColor[] = {0, 12, 255}; +uint8_t lavaColor[] = {255, 30, 0}; +uint8_t honeyColor[] = {255, 172, 0}; +uint8_t shimmerColor[] = {155, 112, 233}; + +uint8_t surfaceColor[] = {155, 209, 255}; +uint8_t undergroundColor[] = {84, 57, 42}; +uint8_t cavernColor[] = {72, 64, 57}; +uint8_t underworldColor[] = {51, 0, 0}; +} // namespace + +Color::Color(uint8_t *rgb) +{ + set(rgb[0], rgb[1], rgb[2]); +} + +Color::Color(uint8_t r, uint8_t g, uint8_t b) +{ + set(r, g, b); +} + +void Color::set(uint8_t r, uint8_t g, uint8_t b) +{ + abgr = (0xff << 24) | (b << 16) | (g << 8) | r; +} + +uint8_t Color::r() const +{ + return abgr & 0xff; +} + +uint8_t Color::g() const +{ + return (abgr >> 8) & 0xff; +} + +uint8_t Color::b() const +{ + return (abgr >> 16) & 0xff; +} + +Color getLayerColor(int y, World &world) +{ + if (y < world.undergroundLevel) { + return surfaceColor; + } else if (y < world.cavernLevel) { + return undergroundColor; + } else if (y < world.height - 230) { + return cavernColor; + } else { + return underworldColor; + } +} + +inline uint8_t roundToByte(double val) +{ + return std::round(val); +} + +Color blendColors(Color base, Color tint) +{ + return { + roundToByte(0.7 * base.r() + 0.3 * tint.r()), + roundToByte(0.7 * base.g() + 0.3 * tint.g()), + roundToByte(0.7 * base.b() + 0.3 * tint.b()), + }; +} + +Color getTileColor(int x, int y, World &world) +{ + Color color = getLayerColor(y, world); + Tile &tile = world.getTile(x, y); + if (tile.wallId != 0) { + Color wallColor{wallColors + 3 * tile.wallId}; + if (tile.wallPaint != 0) { + wallColor = + blendColors(wallColor, paintColors + 3 * tile.wallPaint); + } + color = tile.echoCoatWall ? blendColors(color, wallColor) : wallColor; + } + switch (tile.liquid) { + case Liquid::water: + color = blendColors(waterColor, color); + break; + case Liquid::lava: + color = blendColors(lavaColor, color); + break; + case Liquid::honey: + color = blendColors(honeyColor, color); + break; + case Liquid::shimmer: + color = blendColors(shimmerColor, color); + break; + default: + break; + } + if (tile.blockId != -1) { + Color blockColor{blockColors + 3 * tile.blockId}; + if (tile.actuated) { + blockColor = blendColors(blockColor, {0, 0, 0}); + } + if (tile.blockPaint != 0) { + blockColor = + blendColors(blockColor, paintColors + 3 * tile.blockPaint); + } + color = + tile.echoCoatBlock ? blendColors(color, blockColor) : blockColor; + } + if (tile.wireRed) { + color = blendColors(color, {255, 0, 0}); + } + if (tile.wireBlue) { + color = blendColors(color, {0, 0, 255}); + } + if (tile.wireGreen) { + color = blendColors(color, {0, 255, 0}); + } + if (tile.wireYellow) { + color = blendColors(color, {255, 255, 0}); + } + return color; +} diff --git a/wasm/src/TileColor.h b/wasm/src/TileColor.h new file mode 100644 index 0000000..f5d0772 --- /dev/null +++ b/wasm/src/TileColor.h @@ -0,0 +1,25 @@ +#ifndef TILECOLOR_H +#define TILECOLOR_H + +#include + +class World; + +class Color +{ +public: + Color() = default; + Color(uint8_t *rgb); + Color(uint8_t r, uint8_t g, uint8_t b); + + void set(uint8_t r, uint8_t g, uint8_t b); + uint8_t r() const; + uint8_t g() const; + uint8_t b() const; + + uint32_t abgr; +}; + +Color getTileColor(int x, int y, World &world); + +#endif // TILECOLOR_H diff --git a/wasm/src/World.cpp b/wasm/src/World.cpp new file mode 100644 index 0000000..c9fd6af --- /dev/null +++ b/wasm/src/World.cpp @@ -0,0 +1,12 @@ +#include "World.h" + +void World::initTiles() +{ + tiles.clear(); + tiles.resize(width * height); +} + +Tile &World::getTile(int x, int y) +{ + return tiles[y + x * height]; +} diff --git a/wasm/src/World.h b/wasm/src/World.h index 7a9ceaa..e752683 100644 --- a/wasm/src/World.h +++ b/wasm/src/World.h @@ -1,13 +1,19 @@ #ifndef WORLD_H #define WORLD_H +#include "Tile.h" #include #include #include class World { + std::vector tiles; + public: + void initTiles(); + Tile &getTile(int x, int y); + int version; int revision; bool isFavorite; diff --git a/wasm/src/WorldLoader.cpp b/wasm/src/WorldLoader.cpp index c4d0a86..808d5a7 100644 --- a/wasm/src/WorldLoader.cpp +++ b/wasm/src/WorldLoader.cpp @@ -1,12 +1,14 @@ -#include "Reader.h" -#include "World.h" +#include "WorldLoader.h" +#include "Reader.h" #include #include -#ifndef __EMSCRIPTEN__ -void printWorld(World &world); -#endif +bool isValidWorldSize(const World &world) +{ + return world.width > 0 && world.height > 0 && world.width < 33600 && + world.height < 9600; +} std::string parseBinaryTime(uint64_t ticks) { @@ -46,6 +48,9 @@ void readProperties(Reader &r, World &world) world.bottom = r.getUint32(); world.height = r.getUint32(); world.width = r.getUint32(); + if (!isValidWorldSize(world)) { + return; + } if (world.version >= 209) { world.gameMode = r.getUint32(); if (world.version >= 222) { @@ -290,223 +295,90 @@ void readProperties(Reader &r, World &world) world.moondialCooldown = r.getUint8(); } -void readWorldFile(const std::string &data) +void readTiles(Reader &r, World &world) { - Reader r{data}; - World world{}; - readProperties(r, world); - -#ifndef __EMSCRIPTEN__ - printWorld(world); -#endif -} - -#ifndef __EMSCRIPTEN__ -#include -#include -#include - -int main() -{ - std::ifstream in("/path/to/Test_World.wld", std::ios::binary); - std::ostringstream sstr; - sstr << in.rdbuf(); - std::string data{sstr.str()}; - readWorldFile(data); + world.initTiles(); + for (int x = 0; x < world.width; ++x) { + for (int y = 0, rle = 0; y < world.height; ++y) { + Tile &tile = world.getTile(x, y); + if (rle > 0) { + tile = world.getTile(x, y - 1); + --rle; + continue; + } + std::array flags{0, 0, 0, 0}; + for (auto &flag : flags) { + flag = r.getUint8(); + if ((flag & 0x01) == 0) { + break; + } + } + if ((flags[0] & 0x02) == 0) { + tile.blockId = -1; + } else { + if ((flags[0] & 0x20) == 0) { + tile.blockId = r.getUint8(); + } else { + tile.blockId = r.getUint16(); + } + if (world.framedTiles[tile.blockId]) { + tile.frameX = r.getUint16(); + tile.frameY = r.getUint16(); + } + if ((flags[2] & 0x08) != 0) { + tile.blockPaint = r.getUint8(); + } + tile.slope = static_cast((flags[1] >> 4) & 0x07); + } + if ((flags[0] & 0x04) == 0) { + tile.wallId = 0; + } else { + tile.wallId = r.getUint8(); + if ((flags[2] & 0x10) != 0) { + tile.wallPaint = r.getUint8(); + } + } + if ((flags[0] & 0x18) == 0x08) { + tile.liquid = + (flags[2] & 0x80) == 0 ? Liquid::water : Liquid::shimmer; + } else if ((flags[0] & 0x18) == 0x10) { + tile.liquid = Liquid::lava; + } else if ((flags[0] & 0x18) == 0x18) { + tile.liquid = Liquid::honey; + } + if (tile.liquid != Liquid::none) { + tile.liquidAmount = r.getUint8(); + } + if ((flags[2] & 0x40) != 0) { + tile.wallId |= r.getUint8() << 8; + } + tile.wireRed = (flags[1] & 0x02) != 0; + tile.wireBlue = (flags[1] & 0x04) != 0; + tile.wireGreen = (flags[1] & 0x08) != 0; + tile.wireYellow = (flags[2] & 0x20) != 0; + tile.actuator = (flags[2] & 0x02) != 0; + tile.actuated = (flags[2] & 0x04) != 0; + tile.echoCoatBlock = (flags[3] & 0x02) != 0; + tile.echoCoatWall = (flags[3] & 0x04) != 0; + tile.illuminantBlock = (flags[3] & 0x08) != 0; + tile.illuminantWall = (flags[3] & 0x10) != 0; + if ((flags[0] & 0x40) != 0) { + rle = r.getUint8(); + } else if ((flags[0] & 0x80) != 0) { + rle = r.getUint16(); + } + } + } } -#define DUMP(field) std::cout << #field " " << world.field << '\n' -#define DUMP_ARRAY(field) \ - do { \ - std::cout << #field " ["; \ - for (auto val : world.field) { \ - std::cout << val << ','; \ - }; \ - std::cout << "]\n"; \ - } while (0) - -void printWorld(World &world) +World readWorldFile(const std::string &data) { - DUMP(version); - DUMP(revision); - DUMP(isFavorite); - DUMP(framedTiles.size()); - - DUMP(name); - DUMP(seed); - DUMP(generatorVersion); - DUMP(guid); - DUMP(id); - DUMP(left); - DUMP(right); - DUMP(top); - DUMP(bottom); - DUMP(height); - DUMP(width); - DUMP(gameMode); - DUMP(drunkWorld); - DUMP(forTheWorthy); - DUMP(celebrationmk10); - DUMP(theConstant); - DUMP(notTheBees); - DUMP(dontDigUp); - DUMP(noTraps); - DUMP(getFixedBoi); - DUMP(creationTime); - - DUMP(moonType); - DUMP_ARRAY(treeStyleCoords); - DUMP_ARRAY(treeStyles); - DUMP_ARRAY(caveStyleCoords); - DUMP_ARRAY(caveStyles); - DUMP(iceStyle); - DUMP(jungleStyle); - DUMP(underworldStyle); - DUMP_ARRAY(spawn); - DUMP(undergroundLevel); - DUMP(cavernLevel); - DUMP(gameTime); - DUMP(isDay); - DUMP(moonPhase); - DUMP(bloodMoon); - DUMP(eclipse); - DUMP_ARRAY(dungeon); - DUMP(isCrimson); - - DUMP(downedEyeOfCthulu); - DUMP(downedEaterOfWorlds); - DUMP(downedSkeletron); - DUMP(downedQueenBee); - DUMP(downedTheDestroyer); - DUMP(downedTheTwins); - DUMP(downedSkeletronPrime); - DUMP(downedAnyHardmodeBoss); - DUMP(downedPlantera); - DUMP(downedGolem); - DUMP(downedSlimeKing); - DUMP(savedGoblinTinkerer); - DUMP(savedWizard); - DUMP(savedMechanic); - DUMP(defeatedGoblinInvasion); - DUMP(downedClown); - DUMP(defeatedFrostLegion); - DUMP(defeatedPirates); - - DUMP(brokeAShadowOrb); - DUMP(meteorSpawned); - DUMP(shadowOrbsBroken); - DUMP(altarsSmashed); - DUMP(hardMode); - DUMP(partyOfDoom); - DUMP(goblinInvasionDelay); - DUMP(goblinInvasionSize); - DUMP(goblinInvasionType); - DUMP(goblinInvasionX); - DUMP(slimeRainTime); - DUMP(sundialCooldown); - DUMP(raining); - DUMP(rainTimeLeft); - DUMP(maxRain); - DUMP(cobaltVariant); - DUMP(mythrilVariant); - DUMP(adamantiteVariant); - DUMP(forestStyle1); - DUMP(corruptionStyle); - DUMP(undergroundJungleStyle); - DUMP(snowStyle); - DUMP(hallowStyle); - DUMP(crimsonStyle); - DUMP(desertStyle); - DUMP(oceanStyle); - DUMP(cloudBackground); - DUMP(numberOfClouds); - DUMP(windSpeed); - - DUMP_ARRAY(anglersFinishedDailyQuest); - DUMP(savedAngler); - DUMP(anglerQuest); - DUMP(savedStylist); - DUMP(savedTaxCollector); - DUMP(savedGolfer); - DUMP(invasionStartSize); - DUMP(cultistDelay); - DUMP(enemyKillTallies.size()); - DUMP(fastForwardTimeToDawn); - - DUMP(downedFishron); - DUMP(downedMartians); - DUMP(downedLunaticCultist); - DUMP(downedMoonlord); - DUMP(downedHalloweenPumpking); - DUMP(downedHalloweenMourningWood); - DUMP(downedChristmasIceQueen); - DUMP(downedChristmasSantaNK1); - DUMP(downedChristmasEverscream); - DUMP(downedTowerSolar); - DUMP(downedTowerVortex); - DUMP(downedTowerNebula); - DUMP(downedTowerStardust); - DUMP(towerActiveSolar); - DUMP(towerActiveVortex); - DUMP(towerActiveNebula); - DUMP(towerActiveStardust); - DUMP(lunarApocalypseIsUp); - - DUMP(partyManual); - DUMP(partyGenuine); - DUMP(partyCooldown); - DUMP_ARRAY(partyingNPCs); - DUMP(sandstormActive); - DUMP(sandstormTimeLeft); - DUMP(sandstormSeverity); - DUMP(sandstormIntendedSeverity); - DUMP(savedBartender); - DUMP(downedInvasionTier1); - DUMP(downedInvasionTier2); - DUMP(downedInvasionTier3); - DUMP(mushroomStyle); - DUMP(underworldStyle2); - DUMP(forestStyle2); - DUMP(forestStyle3); - DUMP(forestStyle4); - DUMP(combatBookUsed); - DUMP(lanternNightCooldown); - DUMP(lanternNightGenuine); - DUMP(lanternNightManual); - DUMP(lanternNightNextIsGenuine); - DUMP_ARRAY(treeTopVariations); - DUMP(forceHalloween); - DUMP(forceChristmas); - DUMP(copperVariant); - DUMP(ironVariant); - DUMP(silverVariant); - DUMP(goldVariant); - DUMP(boughtCat); - DUMP(boughtDog); - DUMP(boughtBunny); - - DUMP(downedEmpressOfLight); - DUMP(downedQueenSlime); - DUMP(downedDeerclops); - DUMP(unlockedSlimeBlue); - DUMP(unlockedMerchant); - DUMP(unlockedDemolitionist); - DUMP(unlockedPartyGirl); - DUMP(unlockedDyeTrader); - DUMP(unlockedTruffle); - DUMP(unlockedArmsDealer); - DUMP(unlockedNurse); - DUMP(unlockedPrincess); - DUMP(combatBookVolumeTwoUsed); - DUMP(peddlersSatchelUsed); - DUMP(unlockedSlimeGreen); - DUMP(unlockedSlimeOld); - DUMP(unlockedSlimePurple); - DUMP(unlockedSlimeRainbow); - DUMP(unlockedSlimeRed); - DUMP(unlockedSlimeYellow); - DUMP(unlockedSlimeCopper); - DUMP(fastForwardTimeToDusk); - DUMP(moondialCooldown); + Reader r{data}; + World world{}; + readProperties(r, world); + if (!isValidWorldSize(world)) { + return world; + } + readTiles(r, world); + return world; } -#endif diff --git a/wasm/src/WorldLoader.h b/wasm/src/WorldLoader.h new file mode 100644 index 0000000..ebff487 --- /dev/null +++ b/wasm/src/WorldLoader.h @@ -0,0 +1,8 @@ +#ifndef WORLDLOADER_H +#define WORLDLOADER_H + +#include "World.h" + +World readWorldFile(const std::string &data); + +#endif // WORLDLOADER_H diff --git a/wasm/src/header.js b/wasm/src/header.js new file mode 100644 index 0000000..a5156a1 --- /dev/null +++ b/wasm/src/header.js @@ -0,0 +1 @@ +var Module = {}; diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp new file mode 100644 index 0000000..8accfc2 --- /dev/null +++ b/wasm/src/main.cpp @@ -0,0 +1,249 @@ +#include "WorldLoader.h" + +#ifdef __EMSCRIPTEN__ +#include + +#define DUMP(field) result.set(#field, world.field) +#define DUMP_ARRAY(field) \ + result.set( \ + #field, \ + emscripten::val::array(world.field.begin(), world.field.end())) + +emscripten::val dumpWorld(const World &world) +{ + auto result = emscripten::val::object(); +#else +#include +#include +#include + +#define DUMP(field) std::cout << #field " " << world.field << '\n' +#define DUMP_ARRAY(field) \ + do { \ + std::cout << #field " ["; \ + for (auto val : world.field) { \ + std::cout << val << ','; \ + }; \ + std::cout << "]\n"; \ + } while (0) + +void dumpWorld(const World &world) +{ +#endif + DUMP(version); + DUMP(revision); + DUMP(isFavorite); + DUMP_ARRAY(framedTiles); + + DUMP(name); + DUMP(seed); + DUMP(generatorVersion); + DUMP(guid); + DUMP(id); + DUMP(left); + DUMP(right); + DUMP(top); + DUMP(bottom); + DUMP(height); + DUMP(width); + DUMP(gameMode); + DUMP(drunkWorld); + DUMP(forTheWorthy); + DUMP(celebrationmk10); + DUMP(theConstant); + DUMP(notTheBees); + DUMP(dontDigUp); + DUMP(noTraps); + DUMP(getFixedBoi); + DUMP(creationTime); + + DUMP(moonType); + DUMP_ARRAY(treeStyleCoords); + DUMP_ARRAY(treeStyles); + DUMP_ARRAY(caveStyleCoords); + DUMP_ARRAY(caveStyles); + DUMP(iceStyle); + DUMP(jungleStyle); + DUMP(underworldStyle); + DUMP_ARRAY(spawn); + DUMP(undergroundLevel); + DUMP(cavernLevel); + DUMP(gameTime); + DUMP(isDay); + DUMP(moonPhase); + DUMP(bloodMoon); + DUMP(eclipse); + DUMP_ARRAY(dungeon); + DUMP(isCrimson); + + DUMP(downedEyeOfCthulu); + DUMP(downedEaterOfWorlds); + DUMP(downedSkeletron); + DUMP(downedQueenBee); + DUMP(downedTheDestroyer); + DUMP(downedTheTwins); + DUMP(downedSkeletronPrime); + DUMP(downedAnyHardmodeBoss); + DUMP(downedPlantera); + DUMP(downedGolem); + DUMP(downedSlimeKing); + DUMP(savedGoblinTinkerer); + DUMP(savedWizard); + DUMP(savedMechanic); + DUMP(defeatedGoblinInvasion); + DUMP(downedClown); + DUMP(defeatedFrostLegion); + DUMP(defeatedPirates); + + DUMP(brokeAShadowOrb); + DUMP(meteorSpawned); + DUMP(shadowOrbsBroken); + DUMP(altarsSmashed); + DUMP(hardMode); + DUMP(partyOfDoom); + DUMP(goblinInvasionDelay); + DUMP(goblinInvasionSize); + DUMP(goblinInvasionType); + DUMP(goblinInvasionX); + DUMP(slimeRainTime); + DUMP(sundialCooldown); + DUMP(raining); + DUMP(rainTimeLeft); + DUMP(maxRain); + DUMP(cobaltVariant); + DUMP(mythrilVariant); + DUMP(adamantiteVariant); + DUMP(forestStyle1); + DUMP(corruptionStyle); + DUMP(undergroundJungleStyle); + DUMP(snowStyle); + DUMP(hallowStyle); + DUMP(crimsonStyle); + DUMP(desertStyle); + DUMP(oceanStyle); + DUMP(cloudBackground); + DUMP(numberOfClouds); + DUMP(windSpeed); + + DUMP_ARRAY(anglersFinishedDailyQuest); + DUMP(savedAngler); + DUMP(anglerQuest); + DUMP(savedStylist); + DUMP(savedTaxCollector); + DUMP(savedGolfer); + DUMP(invasionStartSize); + DUMP(cultistDelay); + DUMP_ARRAY(enemyKillTallies); + DUMP(fastForwardTimeToDawn); + + DUMP(downedFishron); + DUMP(downedMartians); + DUMP(downedLunaticCultist); + DUMP(downedMoonlord); + DUMP(downedHalloweenPumpking); + DUMP(downedHalloweenMourningWood); + DUMP(downedChristmasIceQueen); + DUMP(downedChristmasSantaNK1); + DUMP(downedChristmasEverscream); + DUMP(downedTowerSolar); + DUMP(downedTowerVortex); + DUMP(downedTowerNebula); + DUMP(downedTowerStardust); + DUMP(towerActiveSolar); + DUMP(towerActiveVortex); + DUMP(towerActiveNebula); + DUMP(towerActiveStardust); + DUMP(lunarApocalypseIsUp); + + DUMP(partyManual); + DUMP(partyGenuine); + DUMP(partyCooldown); + DUMP_ARRAY(partyingNPCs); + DUMP(sandstormActive); + DUMP(sandstormTimeLeft); + DUMP(sandstormSeverity); + DUMP(sandstormIntendedSeverity); + DUMP(savedBartender); + DUMP(downedInvasionTier1); + DUMP(downedInvasionTier2); + DUMP(downedInvasionTier3); + DUMP(mushroomStyle); + DUMP(underworldStyle2); + DUMP(forestStyle2); + DUMP(forestStyle3); + DUMP(forestStyle4); + DUMP(combatBookUsed); + DUMP(lanternNightCooldown); + DUMP(lanternNightGenuine); + DUMP(lanternNightManual); + DUMP(lanternNightNextIsGenuine); + DUMP_ARRAY(treeTopVariations); + DUMP(forceHalloween); + DUMP(forceChristmas); + DUMP(copperVariant); + DUMP(ironVariant); + DUMP(silverVariant); + DUMP(goldVariant); + DUMP(boughtCat); + DUMP(boughtDog); + DUMP(boughtBunny); + + DUMP(downedEmpressOfLight); + DUMP(downedQueenSlime); + DUMP(downedDeerclops); + DUMP(unlockedSlimeBlue); + DUMP(unlockedMerchant); + DUMP(unlockedDemolitionist); + DUMP(unlockedPartyGirl); + DUMP(unlockedDyeTrader); + DUMP(unlockedTruffle); + DUMP(unlockedArmsDealer); + DUMP(unlockedNurse); + DUMP(unlockedPrincess); + DUMP(combatBookVolumeTwoUsed); + DUMP(peddlersSatchelUsed); + DUMP(unlockedSlimeGreen); + DUMP(unlockedSlimeOld); + DUMP(unlockedSlimePurple); + DUMP(unlockedSlimeRainbow); + DUMP(unlockedSlimeRed); + DUMP(unlockedSlimeYellow); + DUMP(unlockedSlimeCopper); + DUMP(fastForwardTimeToDusk); + DUMP(moondialCooldown); +#ifdef __EMSCRIPTEN__ + return result; +} +#else +} +#endif + +#ifdef __EMSCRIPTEN__ +class Loader +{ + World world; + +public: + emscripten::val loadWorldFile(const std::string &data) + { + world = readWorldFile(data); + return dumpWorld(world); + } +}; + +EMSCRIPTEN_BINDINGS(terramap) +{ + emscripten::class_("Loader").constructor().function( + "loadWorldFile", + &Loader::loadWorldFile); +} +#else +int main() +{ + std::ifstream in("/path/to/Test_World.wld", std::ios::binary); + std::ostringstream sstr; + sstr << in.rdbuf(); + std::string data{sstr.str()}; + dumpWorld(readWorldFile(data)); +} +#endif diff --git a/wasm/src/pre.js b/wasm/src/pre.js new file mode 100644 index 0000000..737361c --- /dev/null +++ b/wasm/src/pre.js @@ -0,0 +1,8 @@ +Module.initializedPromise = new Promise((resolve) => { + Module['onRuntimeInitialized'] = resolve; +}); + +Module['terramap'] = async () => { + await Module.initializedPromise; + return new Module['Loader'](); +}; diff --git a/wasm/src/worker.js b/wasm/src/worker.js new file mode 100644 index 0000000..091dae3 --- /dev/null +++ b/wasm/src/worker.js @@ -0,0 +1,16 @@ +self.addEventListener('message', (e) => self.start(e.data)); + +async function start(file) { + const fileReader = new FileReaderSync(); + + self.postMessage({ status: 'Reading world file...' }); + const buffer = fileReader.readAsArrayBuffer(file); + + self.postMessage({ status: 'Loading world file...' }); + if (typeof self.terramap === 'undefined') { + self.terramap = await Module.terramap(); + } + const world = self.terramap.loadWorldFile(buffer); + console.log(world); + self.postMessage({ world }); +} From 1caf6e45914d6807bd1284b015336f1053fc5cc7 Mon Sep 17 00:00:00 2001 From: Alpha Date: Tue, 5 Aug 2025 21:33:03 -0400 Subject: [PATCH 03/13] Wasm: draw to canvas --- resources/js/main.js | 12 +++--------- wasm/src/Makefile | 3 ++- wasm/src/TileColor.cpp | 8 ++++---- wasm/src/main.cpp | 30 +++++++++++++++++++++++++++--- wasm/src/worker.js | 29 ++++++++++++++++++++++++++++- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/resources/js/main.js b/resources/js/main.js index df2f259..1b25e1f 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -5,7 +5,6 @@ var canvas = document.querySelector("#canvas"); var overlayCanvas = document.querySelector("#overlayCanvas"); var selectionCanvas = document.querySelector("#selectionCanvas"); -var ctx = canvas.getContext("2d"); var overlayCtx = overlayCanvas.getContext("2d"); var selectionCtx = selectionCanvas.getContext("2d"); @@ -14,11 +13,6 @@ var selectionCtx = selectionCanvas.getContext("2d"); var blockSelector = document.querySelector("#blocks"); -ctx.msImageSmoothingEnabled = false; -ctx.mozImageSmoothingEnabled = false; -ctx.msImageSmoothingEnabled = false; -ctx.imageSmoothingEnabled = false; - overlayCtx.msImageSmoothingEnabled = false; overlayCtx.mozImageSmoothingEnabled = false; overlayCtx.msImageSmoothingEnabled = false; @@ -869,9 +863,11 @@ function reloadWorld() { if (worker === null) { worker = new Worker('wasm/src/build/terramap.js'); worker.addEventListener('message', onWorldLoaderWorkerMessage); + const offscreen = canvas.transferControlToOffscreen(); + worker.postMessage({ canvas: offscreen }, [offscreen]); } - worker.postMessage(file); + worker.postMessage({ file }); } function onWorldLoaderWorkerMessage(e) { @@ -961,8 +957,6 @@ function onWorldLoaderWorkerMessage(e) { panzoomContainer.width = world.width; panzoomContainer.height = world.height; - canvas.width = world.width; - canvas.height = world.height; overlayCanvas.width = world.width; overlayCanvas.height = world.height; selectionCanvas.width = world.width; diff --git a/wasm/src/Makefile b/wasm/src/Makefile index 6b7ec41..8f17c40 100644 --- a/wasm/src/Makefile +++ b/wasm/src/Makefile @@ -1,6 +1,7 @@ # Config values -CXXFLAGS := -Wall -Wextra -pedantic -Werror -std=c++20 -O2 +CXXFLAGS := -Wall -Wextra -pedantic -Werror -Wno-dollar-in-identifier-extension \ + -std=c++20 -O2 LDFLAGS := -lembind \ -sENVIRONMENT=worker \ -sALLOW_MEMORY_GROWTH=1 \ diff --git a/wasm/src/TileColor.cpp b/wasm/src/TileColor.cpp index dd2f29e..11f01d3 100644 --- a/wasm/src/TileColor.cpp +++ b/wasm/src/TileColor.cpp @@ -217,7 +217,7 @@ uint8_t lavaColor[] = {255, 30, 0}; uint8_t honeyColor[] = {255, 172, 0}; uint8_t shimmerColor[] = {155, 112, 233}; -uint8_t surfaceColor[] = {155, 209, 255}; +uint8_t surfaceColor[] = {132, 170, 248}; uint8_t undergroundColor[] = {84, 57, 42}; uint8_t cavernColor[] = {72, 64, 57}; uint8_t underworldColor[] = {51, 0, 0}; @@ -297,13 +297,13 @@ Color getTileColor(int x, int y, World &world) color = blendColors(waterColor, color); break; case Liquid::lava: - color = blendColors(lavaColor, color); + color = lavaColor; break; case Liquid::honey: - color = blendColors(honeyColor, color); + color = honeyColor; break; case Liquid::shimmer: - color = blendColors(shimmerColor, color); + color = shimmerColor; break; default: break; diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp index 8accfc2..208bf78 100644 --- a/wasm/src/main.cpp +++ b/wasm/src/main.cpp @@ -1,3 +1,4 @@ +#include "TileColor.h" #include "WorldLoader.h" #ifdef __EMSCRIPTEN__ @@ -229,13 +230,36 @@ class Loader world = readWorldFile(data); return dumpWorld(world); } + + void renderToCanvas() + { + std::vector pixels; + pixels.reserve(world.width * world.height); + for (int y = 0; y < world.height; ++y) { + for (int x = 0; x < world.width; ++x) { + pixels.push_back(getTileColor(x, y, world).abgr); + } + } + EM_ASM_( + { + const data = HEAPU8.slice($0, $0 + $1 * $2 * 4); + const ctx = self['ctx']; + const imageData = ctx.getImageData(0, 0, $1, $2); + imageData.data.set(data); + ctx.putImageData(imageData, 0, 0); + }, + pixels.data(), + world.width, + world.height); + } }; EMSCRIPTEN_BINDINGS(terramap) { - emscripten::class_("Loader").constructor().function( - "loadWorldFile", - &Loader::loadWorldFile); + emscripten::class_("Loader") + .constructor() + .function("loadWorldFile", &Loader::loadWorldFile) + .function("renderToCanvas", &Loader::renderToCanvas); } #else int main() diff --git a/wasm/src/worker.js b/wasm/src/worker.js index 091dae3..e8fee75 100644 --- a/wasm/src/worker.js +++ b/wasm/src/worker.js @@ -1,4 +1,18 @@ -self.addEventListener('message', (e) => self.start(e.data)); +self.addEventListener('message', (e) => { + if (e.data.canvas) { + self.canvas = e.data.canvas; + self.canvas.width = 100; + self.canvas.height = 100; + self.ctx = self.canvas.getContext('2d'); + self.ctx.msImageSmoothingEnabled = false; + self.ctx.mozImageSmoothingEnabled = false; + self.ctx.msImageSmoothingEnabled = false; + self.ctx.imageSmoothingEnabled = false; + } + if (e.data.file) { + self.start(e.data.file); + } +}); async function start(file) { const fileReader = new FileReaderSync(); @@ -12,5 +26,18 @@ async function start(file) { } const world = self.terramap.loadWorldFile(buffer); console.log(world); + if ( + world.width <= 0 || + world.height <= 0 || + world.width > 33600 || + world.height > 9600 + ) { + return; + } + self.canvas.width = world.width; + self.canvas.height = world.height; self.postMessage({ world }); + + self.postMessage({ status: 'Rendering tiles...' }); + self.terramap.renderToCanvas(); } From 67306476d8145ba653ecdcf710fa45eb8865a941 Mon Sep 17 00:00:00 2001 From: Alpha Date: Wed, 6 Aug 2025 22:01:47 -0400 Subject: [PATCH 04/13] Wasm: load chests and NPCs --- resources/js/main.js | 12 ++--- wasm/src/World.h | 36 ++++++++++++++ wasm/src/WorldLoader.cpp | 97 +++++++++++++++++++++++++++++++++++- wasm/src/main.cpp | 103 +++++++++++++++++++++++++++++++++++++++ wasm/src/worker.js | 5 +- 5 files changed, 241 insertions(+), 12 deletions(-) diff --git a/resources/js/main.js b/resources/js/main.js index 1b25e1f..c814d6e 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -721,8 +721,8 @@ function getTileAt(x, y) { } function selectPoint(x, y) { - selectionX = x; - selectionY = y; + selectionX = Math.round(x); + selectionY = Math.round(y); drawSelectionIndicator(); } @@ -926,10 +926,6 @@ function onWorldLoaderWorkerMessage(e) { } } - if(e.data.npcs) { - addNpcs(e.data.npcs); - } - if (e.data.tileEntities) { for (const [pos, entity] of e.data.tileEntities.entries()) { let idx = pos.x * world.height + pos.y; @@ -980,11 +976,13 @@ function onWorldLoaderWorkerMessage(e) { `
  • ${key}: ${Array.isArray(world[key]) ? JSON.stringify(world[key]) : world[key]}
  • ` ) ); + + addNpcs(world.npcs); } } function addNpcs(npcs) { - world.npcs = npcs; + $("#npcList").empty(); for(var i = 0; i < npcs.length; i++) { var npc = npcs[i]; diff --git a/wasm/src/World.h b/wasm/src/World.h index e752683..fdba34e 100644 --- a/wasm/src/World.h +++ b/wasm/src/World.h @@ -6,6 +6,37 @@ #include #include +struct Item { + int id; + int stack; + int prefix; +}; + +struct Chest { + int x; + int y; + std::string name; + std::vector items; +}; + +struct Sign { + int x; + int y; + std::string text; +}; + +struct NPC { + int id; + std::string type; + std::string name; + double x; + double y; + bool isHomeless; + int homeX; + int homeY; + int variation; +}; + class World { std::vector tiles; @@ -195,6 +226,11 @@ class World bool unlockedSlimeCopper; bool fastForwardTimeToDusk; int moondialCooldown; + + std::vector chests; + std::vector signs; + std::vector shimmeredNPCs; + std::vector npcs; }; #endif // WORLD_H diff --git a/wasm/src/WorldLoader.cpp b/wasm/src/WorldLoader.cpp index 808d5a7..72d958e 100644 --- a/wasm/src/WorldLoader.cpp +++ b/wasm/src/WorldLoader.cpp @@ -38,7 +38,7 @@ void readProperties(Reader &r, World &world) if (i == 4 || i == 6 || i == 8 || i == 10) { world.guid += '-'; } - world.guid += std::format("{:x}", r.getUint8()); + world.guid += std::format("{:02x}", r.getUint8()); } } world.id = r.getUint32(); @@ -371,6 +371,98 @@ void readTiles(Reader &r, World &world) } } +void readChests(Reader &r, World &world) +{ + world.chests.resize(r.getUint16()); + int chestSlots = r.getUint16(); + for (Chest &chest : world.chests) { + chest.x = r.getUint32(); + chest.y = r.getUint32(); + chest.name = r.getString(); + for (int slot = 0; slot < chestSlots; ++slot) { + int stack = r.getUint16(); + if (stack > 0) { + chest.items.resize(slot + 1); + Item &item = chest.items[slot]; + item.id = r.getUint32(); + item.stack = stack; + item.prefix = r.getUint8(); + } + } + } +} + +void readSigns(Reader &r, World &world) +{ + world.signs.resize(r.getUint16()); + for (Sign &sign : world.signs) { + sign.text = r.getString(); + sign.x = r.getUint32(); + sign.y = r.getUint32(); + } +} + +void readNPCs(Reader &r, World &world) +{ + if (world.version >= 268) { + for (int i = r.getUint32(); i > 0; --i) { + world.shimmeredNPCs.push_back(r.getUint32()); + } + } + while (r.getBool()) { + NPC npc{}; + if (world.version >= 190) { + npc.id = r.getUint32(); + switch (npc.id) { + // clang-format off + case 17: npc.type = "Merchant"; break; + case 18: npc.type = "Nurse"; break; + case 19: npc.type = "Arms Dealer"; break; + case 20: npc.type = "Dryad"; break; + case 22: npc.type = "Guide"; break; + case 37: npc.type = "Old Man"; break; + case 38: npc.type = "Demolitionist"; break; + case 54: npc.type = "Clothier"; break; + case 107: npc.type = "Goblin Tinkerer"; break; + case 108: npc.type = "Wizard"; break; + case 124: npc.type = "Mechanic"; break; + case 142: npc.type = "Santa Claus"; break; + case 160: npc.type = "Truffle"; break; + case 178: npc.type = "Steampunker"; break; + case 207: npc.type = "Dye Trader"; break; + case 208: npc.type = "Party Girl"; break; + case 209: npc.type = "Cyborg"; break; + case 227: npc.type = "Painter"; break; + case 228: npc.type = "Witch Doctor"; break; + case 229: npc.type = "Pirate"; break; + case 353: npc.type = "Stylist"; break; + case 369: npc.type = "Angler"; break; + case 441: npc.type = "Tax Collector"; break; + case 550: npc.type = "Tavernkeep"; break; + case 588: npc.type = "Golfer"; break; + case 633: npc.type = "Zoologist"; break; + case 637: npc.type = "Town Cat"; break; + case 638: npc.type = "Town Dog"; break; + case 656: npc.type = "Town Bunny"; break; + case 663: npc.type = "Princess"; break; + // clang-format on + } + } else { + npc.type = r.getString(); + } + npc.name = r.getString(); + npc.x = r.getFloat32() / 16; + npc.y = r.getFloat32() / 16; + npc.isHomeless = r.getBool(); + npc.homeX = r.getUint32(); + npc.homeY = r.getUint32(); + if (world.version >= 213 && r.getBool()) { + npc.variation = r.getUint32(); + } + world.npcs.push_back(std::move(npc)); + } +} + World readWorldFile(const std::string &data) { Reader r{data}; @@ -380,5 +472,8 @@ World readWorldFile(const std::string &data) return world; } readTiles(r, world); + readChests(r, world); + readSigns(r, world); + readNPCs(r, world); return world; } diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp index 208bf78..d988b26 100644 --- a/wasm/src/main.cpp +++ b/wasm/src/main.cpp @@ -4,11 +4,64 @@ #ifdef __EMSCRIPTEN__ #include +template emscripten::val marshalData(const std::vector &data) +{ + std::vector jsData; + for (auto &row : data) { + jsData.push_back(marshalData(row)); + } + return emscripten::val::array(jsData); +} + +emscripten::val marshalData(const Item &item) +{ + auto result = emscripten::val::object(); + result.set("id", item.id); + result.set("stack", item.stack); + result.set("prefix", item.prefix); + return result; +} + +emscripten::val marshalData(const Chest &chest) +{ + auto result = emscripten::val::object(); + result.set("x", chest.x); + result.set("y", chest.y); + result.set("name", chest.name); + result.set("items", marshalData(chest.items)); + return result; +} + +emscripten::val marshalData(const Sign &sign) +{ + auto result = emscripten::val::object(); + result.set("x", sign.x); + result.set("y", sign.y); + result.set("text", sign.text); + return result; +} + +emscripten::val marshalData(const NPC &npc) +{ + auto result = emscripten::val::object(); + result.set("id", npc.id); + result.set("type", npc.type); + result.set("name", npc.name); + result.set("x", npc.x); + result.set("y", npc.y); + result.set("isHomeless", npc.isHomeless); + result.set("homeX", npc.homeX); + result.set("homeY", npc.homeY); + result.set("variation", npc.variation); + return result; +} + #define DUMP(field) result.set(#field, world.field) #define DUMP_ARRAY(field) \ result.set( \ #field, \ emscripten::val::array(world.field.begin(), world.field.end())) +#define DUMP_CUSTOM(field) result.set(#field, marshalData(world.field)) emscripten::val dumpWorld(const World &world) { @@ -18,6 +71,45 @@ emscripten::val dumpWorld(const World &world) #include #include +template void marshalData(const std::vector &data) +{ + std::cout << '['; + for (auto &row : data) { + marshalData(row); + std::cout << ','; + } + std::cout << ']'; +} + +void marshalData(const Item &item) +{ + std::cout << "{id:" << item.id << ",stack:" << item.stack + << ",prefix:" << item.prefix << '}'; +} + +void marshalData(const Chest &chest) +{ + std::cout << "{\nx:" << chest.x << ",y:" << chest.y << ",\nname:\"" + << chest.name << "\",\nitems:"; + marshalData(chest.items); + std::cout << "\n}"; +} + +void marshalData(const Sign &sign) +{ + std::cout << "{x:" << sign.x << ",y:" << sign.y << ",text:\"" << sign.text + << "\"}"; +} + +void marshalData(const NPC &npc) +{ + std::cout << "{id:" << npc.id << ",type:\"" << npc.type << "\",name:\"" + << npc.name << "\",x:" << npc.x << ",y:" << npc.y + << ",isHomeless:" << npc.isHomeless << ",homeX:" << npc.homeX + << ",homeY:" << npc.homeY << ",variation:" << npc.variation + << '}'; +} + #define DUMP(field) std::cout << #field " " << world.field << '\n' #define DUMP_ARRAY(field) \ do { \ @@ -27,6 +119,12 @@ emscripten::val dumpWorld(const World &world) }; \ std::cout << "]\n"; \ } while (0) +#define DUMP_CUSTOM(field) \ + do { \ + std::cout << #field " "; \ + marshalData(world.field); \ + std::cout << '\n'; \ + } while (0) void dumpWorld(const World &world) { @@ -212,6 +310,11 @@ void dumpWorld(const World &world) DUMP(unlockedSlimeCopper); DUMP(fastForwardTimeToDusk); DUMP(moondialCooldown); + + DUMP_CUSTOM(chests); + DUMP_CUSTOM(signs); + DUMP_ARRAY(shimmeredNPCs); + DUMP_CUSTOM(npcs); #ifdef __EMSCRIPTEN__ return result; } diff --git a/wasm/src/worker.js b/wasm/src/worker.js index e8fee75..f786a65 100644 --- a/wasm/src/worker.js +++ b/wasm/src/worker.js @@ -1,8 +1,6 @@ self.addEventListener('message', (e) => { if (e.data.canvas) { self.canvas = e.data.canvas; - self.canvas.width = 100; - self.canvas.height = 100; self.ctx = self.canvas.getContext('2d'); self.ctx.msImageSmoothingEnabled = false; self.ctx.mozImageSmoothingEnabled = false; @@ -36,8 +34,7 @@ async function start(file) { } self.canvas.width = world.width; self.canvas.height = world.height; - self.postMessage({ world }); - self.postMessage({ status: 'Rendering tiles...' }); + self.postMessage({ status: 'Rendering tiles...', world }); self.terramap.renderToCanvas(); } From f480a89a867b982abe3be1763b7eb7897086b309 Mon Sep 17 00:00:00 2001 From: Alpha Date: Thu, 7 Aug 2025 00:26:13 -0400 Subject: [PATCH 05/13] Wasm: provide tile hover info --- resources/js/main.js | 21 ++++++------ wasm/src/WorldLoader.cpp | 4 +-- wasm/src/header.js | 1 + wasm/src/main.cpp | 67 ++++++++++++++++++++++++++++++++++++- wasm/src/worker.js | 72 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 13 deletions(-) diff --git a/resources/js/main.js b/resources/js/main.js index c814d6e..f929efb 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -624,6 +624,8 @@ function getItemText(item) { return `${prefix} ${itemName} (${item.count})`; } +var mouseMoveDebounce = null; +var lastMouseMovePos = { x: 0, y: 0 }; panzoomContainer.addEventListener('mousemove', evt => { if(!world) return; @@ -632,16 +634,15 @@ panzoomContainer.addEventListener('mousemove', evt => { var x = mousePos.x; var y = mousePos.y; - $("#status").html(mousePos.x + ',' + (mousePos.y)); - - if(world.tiles) { - var tile = getTileAt(mousePos.x, mousePos.y); - - if(tile) { - var text = getTileText(tile); - - $("#status").html(`${text} (${mousePos.x}, ${mousePos.y})`); - } + clearTimeout(mouseMoveDebounce); + if (Math.hypot(lastMouseMovePos.x - x, lastMouseMovePos.y - y) > 20) { + worker.postMessage({ hoverTile: { x, y } }); + lastMouseMovePos = { x, y }; + } else { + mouseMoveDebounce = setTimeout(() => { + worker.postMessage({ hoverTile: { x, y } }); + lastMouseMovePos = { x, y }; + }, 75); } }); diff --git a/wasm/src/WorldLoader.cpp b/wasm/src/WorldLoader.cpp index 72d958e..69f0568 100644 --- a/wasm/src/WorldLoader.cpp +++ b/wasm/src/WorldLoader.cpp @@ -322,8 +322,8 @@ void readTiles(Reader &r, World &world) tile.blockId = r.getUint16(); } if (world.framedTiles[tile.blockId]) { - tile.frameX = r.getUint16(); - tile.frameY = r.getUint16(); + tile.frameX = static_cast(r.getUint16()); + tile.frameY = static_cast(r.getUint16()); } if ((flags[2] & 0x08) != 0) { tile.blockPaint = r.getUint8(); diff --git a/wasm/src/header.js b/wasm/src/header.js index a5156a1..5ec7291 100644 --- a/wasm/src/header.js +++ b/wasm/src/header.js @@ -1 +1,2 @@ +importScripts('../../../resources/js/settings.js'); var Module = {}; diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp index d988b26..7697fb0 100644 --- a/wasm/src/main.cpp +++ b/wasm/src/main.cpp @@ -56,6 +56,65 @@ emscripten::val marshalData(const NPC &npc) return result; } +std::string marshalData(Liquid liquid) +{ + switch (liquid) { + case Liquid::none: + return ""; + case Liquid::water: + return "Water"; + case Liquid::lava: + return "Lava"; + case Liquid::honey: + return "Honey"; + case Liquid::shimmer: + return "Shimmer"; + } +} + +std::string marshalData(Slope slope) +{ + switch (slope) { + case Slope::none: + return "none"; + case Slope::half: + return "half"; + case Slope::topRight: + return "topRight"; + case Slope::topLeft: + return "topLeft"; + case Slope::bottomRight: + return "bottomRight"; + case Slope::bottomLeft: + return "bottomLeft"; + } +} + +emscripten::val marshalData(const Tile &tile) +{ + auto result = emscripten::val::object(); + result.set("blockId", tile.blockId); + result.set("frameX", tile.frameX); + result.set("frameY", tile.frameY); + result.set("wallId", tile.wallId); + result.set("blockPaint", tile.blockPaint); + result.set("wallPaint", tile.wallPaint); + result.set("liquidAmount", tile.liquidAmount); + result.set("liquid", marshalData(tile.liquid)); + result.set("slope", marshalData(tile.slope)); + result.set("wireRed", tile.wireRed); + result.set("wireBlue", tile.wireBlue); + result.set("wireGreen", tile.wireGreen); + result.set("wireYellow", tile.wireYellow); + result.set("actuated", tile.actuated); + result.set("actuator", tile.actuator); + result.set("echoCoatBlock", tile.echoCoatBlock); + result.set("echoCoatWall", tile.echoCoatWall); + result.set("illuminantBlock", tile.illuminantBlock); + result.set("illuminantWall", tile.illuminantWall); + return result; +} + #define DUMP(field) result.set(#field, world.field) #define DUMP_ARRAY(field) \ result.set( \ @@ -355,6 +414,11 @@ class Loader world.width, world.height); } + + emscripten::val getTile(int x, int y) + { + return marshalData(world.getTile(x, y)); + } }; EMSCRIPTEN_BINDINGS(terramap) @@ -362,7 +426,8 @@ EMSCRIPTEN_BINDINGS(terramap) emscripten::class_("Loader") .constructor() .function("loadWorldFile", &Loader::loadWorldFile) - .function("renderToCanvas", &Loader::renderToCanvas); + .function("renderToCanvas", &Loader::renderToCanvas) + .function("getTile", &Loader::getTile); } #else int main() diff --git a/wasm/src/worker.js b/wasm/src/worker.js index f786a65..d734414 100644 --- a/wasm/src/worker.js +++ b/wasm/src/worker.js @@ -10,6 +10,9 @@ self.addEventListener('message', (e) => { if (e.data.file) { self.start(e.data.file); } + if (e.data.hoverTile) { + onHoverTile(e.data.hoverTile); + } }); async function start(file) { @@ -38,3 +41,72 @@ async function start(file) { self.postMessage({ status: 'Rendering tiles...', world }); self.terramap.renderToCanvas(); } + +function getTileInfo(tile) { + const tileInfo = settings.Tiles[tile.blockId]; + if (tileInfo && tileInfo.Frames) { + for (const frame of tileInfo.Frames.slice().reverse()) { + if ( + ((!frame.U && !tile.frameX) || frame.U <= tile.frameX) && + ((!frame.V && !tile.frameY) || frame.V <= tile.frameY) + ) { + frame.parent = tileInfo; + return frame; + } + } + } + return tileInfo; +} + +function getTileText(tile) { + let text = 'Nothing'; + const tileInfo = getTileInfo(tile); + if (tileInfo) { + if (tileInfo.parent && tileInfo.parent.Name) { + text = tileInfo.parent.Name; + if (tileInfo.Name) { + text += ` - ${tileInfo.Name}`; + } + if (tileInfo.Variety) { + text += ` - ${tileInfo.Variety}`; + } + } else { + text = tileInfo.Name; + } + + if (tile.frameX === 0 && tile.frameY === 0) { + text += ` (${tile.blockId})`; + } else { + text += ` (${tile.blockId}, ${tile.frameX}, ${tile.frameY})`; + } + // TODO: tileEntity + } else if (tile.wallId >= settings.Walls.length) { + text = `Unknown Wall (${tile.wallId})`; + } else if (tile.wallId > 0) { + text = `${settings.Walls[tile.wallId].Name} (${tile.wallId})`; + } + + if (tile.liquid) { + text = text === 'Nothing' ? tile.liquid : `${text} ${tile.liquid}`; + } + const flagFields = [ + ['wireRed', 'Red Wire'], + ['wireBlue', 'Blue Wire'], + ['wireGreen', 'Green Wire'], + ['wireYellow', 'Yellow Wire'], + ['actuator', 'Actuator'], + ]; + for (const [key, label] of flagFields) { + if (tile[key]) { + text += ` (${label})`; + } + } + + return text; +} + +function onHoverTile({ x, y }) { + const tile = self.terramap.getTile(x, y); + const text = getTileText(tile); + self.postMessage({ status: `${text} (${x}, ${y})` }); +} From dbb69f3debdeff5f4d3de612c16f4f9a0d1a96ce Mon Sep 17 00:00:00 2001 From: Alpha Date: Thu, 7 Aug 2025 17:52:49 -0400 Subject: [PATCH 06/13] Wasm: provide tile info --- resources/js/main.js | 162 ++++++++------------------------------- wasm/src/World.h | 12 +++ wasm/src/WorldLoader.cpp | 106 ++++++++++++++++++++++++- wasm/src/main.cpp | 90 +++++++++++++++++++++- wasm/src/worker.js | 39 +++++++++- 5 files changed, 273 insertions(+), 136 deletions(-) diff --git a/resources/js/main.js b/resources/js/main.js index f929efb..f31899a 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -591,15 +591,13 @@ function resizeCanvases() { function getMousePos(canvas, evt) { var rect = panzoomContainer.getBoundingClientRect(); - var transform = $(panzoomContainer).panzoom('getMatrix'); - - var scale = transform[0]; - scale = rect.width / panzoomContainer.width; + const scaleX = rect.width / (panzoomContainer.width + 0.5); + const scaleY = rect.height / (panzoomContainer.height + 0.5); var mousePos = { - x: Math.floor((evt.clientX - rect.left) / scale), - y: Math.floor((evt.clientY - rect.top) / scale) + x: Math.floor((evt.clientX - rect.left) / scaleX), + y: Math.floor((evt.clientY - rect.top) / scaleY) }; // console.log(`${evt.clientX}\t${evt.clientY}\t${rect.left}\t${rect.top}\t${scale}\t${mousePos.x}\t${mousePos.y}`); @@ -610,8 +608,8 @@ function getMousePos(canvas, evt) { function getItemText(item) { let prefix = ""; - if(item.prefixId > 0 && item.prefixId < settings.ItemPrefix.length) - prefix = settings.ItemPrefix[item.prefixId].Name; + if(item.prefix > 0 && item.prefix < settings.ItemPrefix.length) + prefix = settings.ItemPrefix[item.prefix].Name; let itemName = item.id; for(let itemIndex = 0; itemIndex < settings.Items.length; itemIndex++) { @@ -621,7 +619,7 @@ function getItemText(item) { break; } } - return `${prefix} ${itemName} (${item.count})`; + return `${prefix} ${itemName} (${item.stack})`; } var mouseMoveDebounce = null; @@ -657,57 +655,7 @@ $("#panzoomContainer").on('panzoomend', function(evt, panzoom, matrix, changed) selectionY = y; drawSelectionIndicator(); - - var tile = getTileAt(x, y); - if(tile) { - var text = getTileText(tile); - - $("#tileInfoList").html(""); - - var chest = tile.chest; - if(chest) { - if(chest.name.length > 0) - text = `${text} - ${chest.name}`; - - for(var i = 0; i < chest.items.length; i++) { - let item = chest.items[i]; - let itemText = getItemText(item); - - $("#tileInfoList").append(`
  • ${itemText}
  • `); - } - } - - let tileEntity = tile.tileEntity; - if (tileEntity) { - switch (tileEntity.type) { - case 3: // mannequin - case 5: // hat rack - let items = tileEntity.items; - let dyes = tileEntity.dyes; - let itemLength = items.length; - for (let i = 0; i < itemLength; i++) { - let item = items[i]; - if (item.id > 0) { - $("#tileInfoList").append(`
  • ${getItemText(item)}
  • `); - } - let dye = dyes[i]; - if (dye.id > 0) { - $("#tileInfoList").append(`
  • ${getItemText(dye)}
  • `); - } - } - break; - } - } - - var sign = tile.sign; - if(sign && sign.text) { - if(sign.text.length > 0) - $("#tileInfoList").append(`
  • ${sign.text}
  • `); - } - - $("#tile").html(text); - } - + worker.postMessage({ selectTile: { x, y } }); }); function getTileAt(x, y) { @@ -875,78 +823,34 @@ function onWorldLoaderWorkerMessage(e) { if(e.data.status) $("#status").html(e.data.status); - if(e.data.tiles) { - let xlimit = e.data.x + e.data.tiles.length / world.height; - let i = 0; - for(let x = e.data.x; x < xlimit; x++) { - for(let y = 0; y < world.height; y++) { - let tile = e.data.tiles[i++]; - if(tile) { - tile.info = getTileInfo(tile); - world.tiles.push(tile); - - var c = getTileColor(y, tile, world); - if(!c) c = {"r": 0, "g": 0, "b": 0 }; - - ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`; - ctx.fillRect(x, y, 1, 1); - } - } - } - } - - if(e.data.chests) { - world.chests = e.data.chests; - - for(i = 0; i < e.data.chests.length; i++) { - var chest = e.data.chests[i]; - - var idx = chest.x * world.height + chest.y; - world.tiles[idx].chest = chest; - world.tiles[idx + 1].chest = chest; - - idx = (chest.x + 1) * world.height + chest.y; - world.tiles[idx].chest = chest; - world.tiles[idx + 1].chest = chest; - } - } - - if(e.data.signs) { - world.signs = e.data.signs; - - for(i = 0; i < e.data.signs.length; i++) { - var sign = e.data.signs[i]; - - var tileIndex = sign.x * world.height + sign.y; - world.tiles[tileIndex].sign = sign; - world.tiles[tileIndex + 1].sign = sign; - - tileIndex = (sign.x + 1) * world.height + sign.y; - world.tiles[tileIndex].sign = sign; - world.tiles[tileIndex + 1].sign = sign; + if(e.data.tile) { + $("#tileInfoList").empty(); + const tile = e.data.tile; + if (tile.chest) { + if (tile.chest.name.length > 0) { + tile.text += ` - ${tile.chest.name}`; + } + for (const item of tile.chest.items) { + $("#tileInfoList").append(`
  • ${getItemText(item)}
  • `); + } } - } - - if (e.data.tileEntities) { - for (const [pos, entity] of e.data.tileEntities.entries()) { - let idx = pos.x * world.height + pos.y; - let tile = world.tiles[idx]; - if (tile) { - let size = tile.info.Size; - let sizeX = 1; - let sizeY = 1; - if (size) { - sizeX = size[0] - '0'; - sizeY = size[2] - '0'; + if (tile.tileEntity && tile.tileEntity.items.length > 1) { + const items = tile.tileEntity.items; + const dyes = tile.tileEntity.dyes; + for (let i = 0; i < items.length; ++i) { + if (items[i].id > 0) { + $("#tileInfoList").append(`
  • ${getItemText(items[i])}
  • `); } - for (let x = 0; x < sizeX; x++) { - for (let y = 0; y < sizeY; y++) { - let idx = (pos.x+x) * world.height + pos.y+y; - world.tiles[idx].tileEntity = entity; - } + if (dyes[i].id > 0) { + $("#tileInfoList").append(`
  • ${getItemText(dyes[i])}
  • `); } } } + if (tile.sign && tile.sign.text.length > 0) { + const signText = tile.sign.text.trim().replaceAll('\n', '
    '); + $("#tileInfoList").append(`
  • ${signText}
  • `); + } + $("#tile").html(tile.text); } if(e.data.world) { @@ -959,8 +863,6 @@ function onWorldLoaderWorkerMessage(e) { selectionCanvas.width = world.width; selectionCanvas.height = world.height; - world.tiles = []; - resizeCanvases(); $("#worldPropertyList").empty(); @@ -969,7 +871,7 @@ function onWorldLoaderWorkerMessage(e) { const value = world[key]; const type = typeof value; return type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint' || ( - Array.isArray(value) && value.length < 5 + Array.isArray(value) && value.length < 5 && key !== 'npcs' && key !== 'signs' ); }).sort() .forEach(key => diff --git a/wasm/src/World.h b/wasm/src/World.h index fdba34e..1ac3033 100644 --- a/wasm/src/World.h +++ b/wasm/src/World.h @@ -37,6 +37,17 @@ struct NPC { int variation; }; +struct TileEntity { + int id; + int type; + int x; + int y; + int sensorType; + bool sensorActive; + std::vector items; + std::vector dyes; +}; + class World { std::vector tiles; @@ -231,6 +242,7 @@ class World std::vector signs; std::vector shimmeredNPCs; std::vector npcs; + std::vector tileEntities; }; #endif // WORLD_H diff --git a/wasm/src/WorldLoader.cpp b/wasm/src/WorldLoader.cpp index 69f0568..ab53812 100644 --- a/wasm/src/WorldLoader.cpp +++ b/wasm/src/WorldLoader.cpp @@ -12,7 +12,7 @@ bool isValidWorldSize(const World &world) std::string parseBinaryTime(uint64_t ticks) { - uint64_t ms = ticks / 10000 - 62135596800000ull; + uint64_t ms = (ticks & 0x3fffffffffffffffull) / 10000 - 62135596800000ull; auto time = std::chrono::sys_time{std::chrono::milliseconds{ms}}; return std::format("{:%e %B %Y}", time); } @@ -409,6 +409,7 @@ void readNPCs(Reader &r, World &world) world.shimmeredNPCs.push_back(r.getUint32()); } } + // Town NPCs. while (r.getBool()) { NPC npc{}; if (world.version >= 190) { @@ -461,6 +462,108 @@ void readNPCs(Reader &r, World &world) } world.npcs.push_back(std::move(npc)); } + if (world.version >= 140) { + // Enemies. + while (r.getBool()) { + NPC npc{}; + if (world.version >= 190) { + npc.id = r.getUint32(); + } else { + npc.type = r.getString(); + } + npc.x = r.getFloat32() / 16; + npc.y = r.getFloat32() / 16; + world.npcs.push_back(std::move(npc)); + } + } +} + +void readMannequin(TileEntity &entity, Reader &r) +{ + uint8_t itemMask = r.getUint8(); + uint8_t dyeMask = r.getUint8(); + entity.items.resize(8); + for (int i = 0; i < 8; ++i) { + if ((itemMask & (1 << i)) != 0) { + Item &item = entity.items[i]; + item.id = r.getUint16(); + item.prefix = r.getUint8(); + item.stack = r.getUint16(); + } + } + entity.dyes.resize(8); + for (int i = 0; i < 8; ++i) { + if ((dyeMask & (1 << i)) != 0) { + Item &item = entity.dyes[i]; + item.id = r.getUint16(); + item.prefix = r.getUint8(); + item.stack = r.getUint16(); + } + } +} + +void readHatRack(TileEntity &entity, Reader &r) +{ + uint8_t mask = r.getUint8(); + entity.items.resize(2); + for (Item &item : entity.items) { + if ((mask & 1) != 0) { + item.id = r.getUint16(); + item.prefix = r.getUint8(); + item.stack = r.getUint16(); + } + mask >>= 1; + } + entity.dyes.resize(2); + for (Item &item : entity.dyes) { + if ((mask & 1) != 0) { + item.id = r.getUint16(); + item.prefix = r.getUint8(); + item.stack = r.getUint16(); + } + mask >>= 1; + } +} + +void readTileEntities(Reader &r, World &world) +{ + if (world.version < 140) { + return; + } + for (int i = r.getUint32(); i > 0; --i) { + TileEntity entity{}; + entity.type = r.getUint8(); + entity.id = r.getUint32(); + entity.x = r.getUint16(); + entity.y = r.getUint16(); + switch (entity.type) { + case 0: // Target dummy. + r.getUint16(); + break; + case 1: // Item frame. + case 4: // Weapon rack. + case 6: // Plate. + { + Item item; + item.id = r.getUint16(); + item.prefix = r.getUint8(); + item.stack = r.getUint16(); + entity.items.push_back(item); + break; + } + case 2: // Logic sensor. + entity.sensorType = r.getUint8(); + entity.sensorActive = r.getBool(); + break; + case 3: // (Wo)Mannequin. + readMannequin(entity, r); + break; + case 5: // Hat rack. + readHatRack(entity, r); + break; + } + world.tileEntities.push_back(std::move(entity)); + } } World readWorldFile(const std::string &data) @@ -475,5 +578,6 @@ World readWorldFile(const std::string &data) readChests(r, world); readSigns(r, world); readNPCs(r, world); + readTileEntities(r, world); return world; } diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp index 7697fb0..2971afc 100644 --- a/wasm/src/main.cpp +++ b/wasm/src/main.cpp @@ -115,6 +115,20 @@ emscripten::val marshalData(const Tile &tile) return result; } +emscripten::val marshalData(const TileEntity &entity) +{ + auto result = emscripten::val::object(); + result.set("id", entity.id); + result.set("type", entity.type); + result.set("x", entity.x); + result.set("y", entity.y); + result.set("sensorType", entity.sensorType); + result.set("sensorActive", entity.sensorActive); + result.set("items", marshalData(entity.items)); + result.set("dyes", marshalData(entity.dyes)); + return result; +} + #define DUMP(field) result.set(#field, world.field) #define DUMP_ARRAY(field) \ result.set( \ @@ -385,11 +399,66 @@ void dumpWorld(const World &world) class Loader { World world; + std::map chestLookup; + std::map signLookup; + std::map entityLookup; public: emscripten::val loadWorldFile(const std::string &data) { world = readWorldFile(data); + chestLookup.clear(); + for (size_t chestId = 0; chestId < world.chests.size(); ++chestId) { + const Chest &chest = world.chests[chestId]; + int maxI = world.getTile(chest.x, chest.y).blockId == 88 ? 3 : 2; + for (int i = 0; i < maxI; ++i) { + for (int j = 0; j < 2; ++j) { + chestLookup[(chest.x + i) * world.height + chest.y + j] = + chestId; + } + } + } + signLookup.clear(); + for (size_t signId = 0; signId < world.signs.size(); ++signId) { + const Sign &sign = world.signs[signId]; + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + signLookup[(sign.x + i) * world.height + sign.y + j] = + signId; + } + } + } + entityLookup.clear(); + for (size_t entityId = 0; entityId < world.tileEntities.size(); + ++entityId) { + const TileEntity &entity = world.tileEntities[entityId]; + int maxI = 1; + int maxJ = 1; + switch (entity.type) { + case 1: // Item frame. + maxI = 2; + maxJ = 2; + break; + case 3: // (Wo)Mannequin. + maxI = 2; + maxJ = 3; + break; + case 4: // Weapon rack. + maxI = 3; + maxJ = 3; + break; + case 5: // Hat rack. + maxI = 3; + maxJ = 4; + break; + } + for (int i = 0; i < maxI; ++i) { + for (int j = 0; j < maxJ; ++j) { + entityLookup[(entity.x + i) * world.height + entity.y + j] = + entityId; + } + } + } return dumpWorld(world); } @@ -417,7 +486,26 @@ class Loader emscripten::val getTile(int x, int y) { - return marshalData(world.getTile(x, y)); + if (x < 0 || y < 0 || x >= world.width || y >= world.height) { + return emscripten::val::null(); + } + auto result = marshalData(world.getTile(x, y)); + int lookupKey = x * world.height + y; + auto chestItr = chestLookup.find(lookupKey); + if (chestItr != chestLookup.end()) { + result.set("chest", marshalData(world.chests[chestItr->second])); + } + auto signItr = signLookup.find(lookupKey); + if (signItr != signLookup.end()) { + result.set("sign", marshalData(world.signs[signItr->second])); + } + auto entityItr = entityLookup.find(lookupKey); + if (entityItr != entityLookup.end()) { + result.set( + "tileEntity", + marshalData(world.tileEntities[entityItr->second])); + } + return result; } }; diff --git a/wasm/src/worker.js b/wasm/src/worker.js index d734414..af0d408 100644 --- a/wasm/src/worker.js +++ b/wasm/src/worker.js @@ -13,6 +13,9 @@ self.addEventListener('message', (e) => { if (e.data.hoverTile) { onHoverTile(e.data.hoverTile); } + if (e.data.selectTile) { + onSelectTile(e.data.selectTile); + } }); async function start(file) { @@ -79,7 +82,22 @@ function getTileText(tile) { } else { text += ` (${tile.blockId}, ${tile.frameX}, ${tile.frameY})`; } - // TODO: tileEntity + if (tile.tileEntity) { + const entity = tile.tileEntity; + if (entity.type === 2) { + const sensorType = tileInfo.CheckTypes[entity.sensorType]; + const on = entity.sensorActive ? 'On' : 'Off'; + text += ` - ${sensorType}, ${on}`; + } else if (entity.items.length === 1) { + const itemId = entity.items[0].id; + for (const itemSettings of settings.Items) { + if (Number(itemSettings.Id) === itemId) { + text += ` - ${itemSettings.Name}`; + break; + } + } + } + } } else if (tile.wallId >= settings.Walls.length) { text = `Unknown Wall (${tile.wallId})`; } else if (tile.wallId > 0) { @@ -96,17 +114,30 @@ function getTileText(tile) { ['wireYellow', 'Yellow Wire'], ['actuator', 'Actuator'], ]; + const flags = []; for (const [key, label] of flagFields) { if (tile[key]) { - text += ` (${label})`; + flags.push(label); } } + if (flags.length > 0) { + text += ` (${flags.join(', ')})`; + } return text; } function onHoverTile({ x, y }) { const tile = self.terramap.getTile(x, y); - const text = getTileText(tile); - self.postMessage({ status: `${text} (${x}, ${y})` }); + if (tile) { + self.postMessage({ status: `${getTileText(tile)} (${x}, ${y})` }); + } +} + +function onSelectTile({ x, y }) { + const tile = self.terramap.getTile(x, y); + if (tile) { + tile.text = getTileText(tile); + self.postMessage({ tile }); + } } From ed5dfae6f87c417be6ed10b946d7d1cb8e17daa6 Mon Sep 17 00:00:00 2001 From: Alpha Date: Sat, 9 Aug 2025 11:45:06 -0400 Subject: [PATCH 07/13] Wasm: provide search --- resources/js/main.js | 316 +++---------------------------------------- wasm/src/main.cpp | 179 ++++++++++++++++++++++-- wasm/src/worker.js | 59 +++++++- 3 files changed, 244 insertions(+), 310 deletions(-) diff --git a/resources/js/main.js b/resources/js/main.js index f31899a..e9cebdb 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -5,19 +5,10 @@ var canvas = document.querySelector("#canvas"); var overlayCanvas = document.querySelector("#overlayCanvas"); var selectionCanvas = document.querySelector("#selectionCanvas"); -var overlayCtx = overlayCanvas.getContext("2d"); var selectionCtx = selectionCanvas.getContext("2d"); -// var canvasContextImageData = ctx.createImageData(1,1); -// var imageData = canvasContextImageData.data; - var blockSelector = document.querySelector("#blocks"); -overlayCtx.msImageSmoothingEnabled = false; -overlayCtx.mozImageSmoothingEnabled = false; -overlayCtx.msImageSmoothingEnabled = false; -overlayCtx.imageSmoothingEnabled = false; - selectionCtx.msImageSmoothingEnabled = false; selectionCtx.mozImageSmoothingEnabled = false; selectionCtx.msImageSmoothingEnabled = false; @@ -117,7 +108,7 @@ function highlightSet(setIndex) { // blockSelector.val(selectedValues); - highlightInfos(set.Entries); + worker.postMessage({ search: set.Entries }); } function sortAndAddSelectOptions() { @@ -316,137 +307,15 @@ function nextBlock(e) { findBlock(1); } -function isTileMatch(tile, selectedInfos, x, y) { - for(var j = 0; j < selectedInfos.length; j++) { - var info = selectedInfos[j]; - - // check the tile first - if(tile.info && info.isTile && (tile.info == info || (!info.parent && tile.Type == info.Id))) - return true; - - // check the wall - if(info.isWall && tile.WallType == info.Id) - return true; - - // see if it's a chest - var chest = tile.chest; - if(chest && info.isItem) { - // see if the chest contains the item - for(var i = 0; i < chest.items.length; i++) { - var item = chest.items[i]; - - if(info.Id == item.id) { - return true; - } - } - } - - // check if the tile entity contains it - let tileEntity = tile.tileEntity; - if (tileEntity && info.isItem) { - switch (tileEntity.type) { - case 1: // item frame - case 4: // weapon rack - case 6: // plate - if (info.Id == tileEntity.item.id) { - return true; - } - break; - case 3: // (wo)mannequin - case 5: // hat rack - for (let i = 0; i < tileEntity.items.length; i++) { - if (info.Id == tileEntity.items[i].id) { - return true; - } - if (info.Id == tileEntity.dyes[i].id) { - return true; - } - } - break; - } - } - } - - return false; -} - function findBlock(direction) { - if(!world) - return; - - var x = selectionX; - var y = selectionY + direction; - - var start = x * world.height + y; - - var selectedInfos = getSelectedInfos(); - - if(selectedInfos.length > 0) { - for(var i = start; i >= 0 && i < world.tiles.length; i += direction) { - var tile = world.tiles[i]; - - var foundMatch = false; - - if(isTileMatch(tile, selectedInfos, x, y)) { - selectionX = x; - selectionY = y; - - drawSelectionIndicator(); - // panzoom.panzoom('pan', (-overlayCanvas.width / 2) - x, (-overlayCanvas.height / 2) - y, { relative: false }); - - foundMatch = true; - - break; - } - - y += direction; - - if(y < 0 || y >= world.height) { - if(direction > 0) - y = 0; - else - y = world.height - 1; - x += direction; - } - - if(foundMatch) - break; - } - } + worker?.postMessage?.({ + findNext: { x: selectionX, y: selectionY, direction }, + }); } function highlightAll() { - if(!world) - return; - - var selectedInfos = getSelectedInfos(); - - highlightInfos(selectedInfos); -} - -function highlightInfos(selectedInfos) { - var x = 0; - var y = 0; - - overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); - overlayCtx.fillStyle = "rgba(0, 0, 0, 0.75)"; - overlayCtx.fillRect(0, 0, overlayCanvas.width, overlayCanvas.height); - - if(selectedInfos.length > 0) { - for(var i = 0; i < world.tiles.length; i++) { - var tile = world.tiles[i]; - - if(isTileMatch(tile, selectedInfos)) { - overlayCtx.fillStyle = "rgb(255, 255, 255)"; - overlayCtx.fillRect(x, y, 1, 1); - } - - y++; - if(y >= world.height) { - y = 0; - x++; - } - } + if (world) { + worker.postMessage({ search: getSelectedInfos() }); } } @@ -535,36 +404,9 @@ function getWallInfoFromOption(option) { return null; } -function getTileInfo(tile) { - var tileInfo = settings.Tiles[tile.Type]; - - if(!tileInfo) return tileInfo; - - if(!tileInfo.Frames) - return tileInfo; - - var matchingFrame; - - for(var i = 0; i < tileInfo.Frames.length; i++) { - var frame = tileInfo.Frames[i]; - - if((!frame.U && !tile.TextureU) || frame.U <= tile.TextureU) { - if((!frame.V && !tile.TextureV) || frame.V <= tile.TextureV) - matchingFrame = frame; - } - } - - if(!matchingFrame) - return tileInfo; - - matchingFrame.parent = tileInfo; - - return matchingFrame; -} - function clearHighlight() { - overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); selectionCtx.clearRect(0, 0, selectionCanvas.width, selectionCanvas.height); + worker?.postMessage?.({ clearHighlight: true }); } function clearSelection() { @@ -708,98 +550,6 @@ function drawSelectionIndicator() { selectionCtx.stroke(); } -function getTileText (tile) { - var text = "Nothing"; - - if(!tile) { - return text; - } - - var tileInfo = tile.info; - - if(tileInfo) { - if(!tileInfo.parent || !tileInfo.parent.Name) { - text = tileInfo.Name; - } - else if(tileInfo.parent && tileInfo.parent.Name) { - text = tileInfo.parent.Name; - - if(tileInfo.Name) { - text = `${text} - ${tileInfo.Name}`; - - if(tileInfo.Variety) - text = `${text} - ${tileInfo.Variety}`; - } - else if (tileInfo.Variety) { - text = `${text} - ${tileInfo.Variety}`; - } - } - - if(tile.TextureU > 0 && tile.TextureV > 0) - text = `${text} (${tile.Type}, ${tile.TextureU}, ${tile.TextureV})`; - else if(tile.TextureU > 0) - text = `${text} (${tile.Type}, ${tile.TextureU})`; - else - text = `${text} (${tile.Type})`; - if (tile.tileEntity) { - let tileEntity = tile.tileEntity; - switch (tileEntity.type) { - case 1: // item frame - case 4: // weapon rack - case 6: // plate - let item = tileEntity.item; - let itemText = getItemText(item); - text = `${text} - ${itemText}`; - break; - case 2: // logic sensor - let checkType = tile.info.CheckTypes[tileEntity.logicCheckType]; - let on = tileEntity.on ? "On" : "Off"; - text = `${text} - ${checkType}, ${on}`; - break; - } - } - } - else if (tile.WallType || tile.WallType === 0) { - if(tile.WallType < settings.Walls.length) { - text = `${settings.Walls[tile.WallType].Name} (${tile.WallType})`; - } - else { - text = `Unknown Wall (${tile.WallType})`; - } - } - - if (tile.IsLiquidPresent) { - if (text === "Nothing") text = ""; - - if(tile.IsLiquidLava) { - text += text ? " Lava" : "Lava"; - } - else if (tile.IsLiquidHoney) { - text += text ? " Honey" : "Honey"; - } - else if (tile.Shimmer) { - text += text ? " Shimmer" : "Shimmer"; - } - else { - text += text ? " Water" : "Water"; - } - } - - if(tile.IsRedWirePresent) - text += " (Red Wire)"; - - if(tile.IsGreenWirePresent) - text += " (Green Wire)"; - - if(tile.IsBlueWirePresent) - text += " (Blue Wire)"; - - if(tile.IsYellowWirePresent) - text += " (Yellow Wire)"; - - return text; -} - function fileNameChanged (evt) { file = evt.target.files[0]; @@ -813,7 +563,11 @@ function reloadWorld() { worker = new Worker('wasm/src/build/terramap.js'); worker.addEventListener('message', onWorldLoaderWorkerMessage); const offscreen = canvas.transferControlToOffscreen(); - worker.postMessage({ canvas: offscreen }, [offscreen]); + const offscreenOverlay = overlayCanvas.transferControlToOffscreen(); + worker.postMessage({ canvas: offscreen, overlayCanvas: offscreenOverlay }, [ + offscreen, + offscreenOverlay, + ]); } worker.postMessage({ file }); @@ -823,7 +577,7 @@ function onWorldLoaderWorkerMessage(e) { if(e.data.status) $("#status").html(e.data.status); - if(e.data.tile) { + if (e.data.tile) { $("#tileInfoList").empty(); const tile = e.data.tile; if (tile.chest) { @@ -858,8 +612,6 @@ function onWorldLoaderWorkerMessage(e) { panzoomContainer.width = world.width; panzoomContainer.height = world.height; - overlayCanvas.width = world.width; - overlayCanvas.height = world.height; selectionCanvas.width = world.width; selectionCanvas.height = world.height; @@ -882,6 +634,10 @@ function onWorldLoaderWorkerMessage(e) { addNpcs(world.npcs); } + + if (e.data.select) { + selectPoint(e.data.select.x, e.data.select.y); + } } function addNpcs(npcs) { @@ -890,47 +646,15 @@ function addNpcs(npcs) { for(var i = 0; i < npcs.length; i++) { var npc = npcs[i]; - var npcText = npc.name; - if(npc.type != npc.name) { - npcText = `${npcText} the ${npc.type}`; + var npcText = npc.type; + if (npc.name && npc.type != npc.name) { + npcText = `${npc.name} the ${npc.type}`; } $("#npcList").append(`
  • ${npcText}
  • `); } } -function getTileColor(y, tile, world) { - if(tile.IsActive) { - return tileColors[tile.Type][0]; - } - - if (tile.IsLiquidPresent) { - if(tile.IsLiquidLava) - return liquidColors[1]; - else if (tile.IsLiquidHoney) - return liquidColors[2]; - else if (tile.Shimmer) - return { "r": 155, "g": 112, "b": 233 }; - else - return liquidColors[0]; - } - - if (tile.IsWallPresent) { - return wallColors[tile.WallType][0]; - } - - if(y < world.worldSurfaceY) - return { "r": 132, "g": 170, "b": 248 }; - - if(y < world.rockLayerY) - return { "r": 88, "g": 61, "b": 46 }; - - if(y < world.hellLayerY) - return { "r": 74, "g": 67, "b": 60 }; - - return { "r": 0, "g": 0, "b": 0 }; -} - function saveMapImage() { var newCanvas = document.createElement("canvas"); var newContext = newCanvas.getContext("2d"); diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp index 2971afc..d16315f 100644 --- a/wasm/src/main.cpp +++ b/wasm/src/main.cpp @@ -1,10 +1,19 @@ #include "TileColor.h" #include "WorldLoader.h" +#include #ifdef __EMSCRIPTEN__ #include -template emscripten::val marshalData(const std::vector &data) +template emscripten::val marshalData(const std::pair &pair) +{ + return emscripten::val::array(std::vector{pair.first, pair.second}); +} + +template < + typename C, + typename T = std::decay_t()))>> +emscripten::val marshalData(const C &data) { std::vector jsData; for (auto &row : data) { @@ -402,6 +411,99 @@ class Loader std::map chestLookup; std::map signLookup; std::map entityLookup; + std::vector> searchResults; + + void searchBlocks( + const std::set &blockIds, + std::set> &results) + { + for (int x = 0; x < world.width; ++x) { + for (int y = 0; y < world.height; ++y) { + if (blockIds.contains(world.getTile(x, y).blockId)) { + results.emplace(x, y); + } + } + } + } + + void searchFramedBlocks( + int blockId, + int minU, + int maxU, + int minV, + int maxV, + std::set> &results) + { + for (int x = 0; x < world.width; ++x) { + for (int y = 0; y < world.height; ++y) { + Tile &tile = world.getTile(x, y); + if (tile.blockId == blockId && tile.frameX >= minU && + tile.frameX < maxU && tile.frameY >= minV && + tile.frameY < maxV) { + results.emplace(x, y); + } + } + } + } + + void searchWalls(int wallId, std::set> &results) + { + for (int x = 0; x < world.width; ++x) { + for (int y = 0; y < world.height; ++y) { + if (world.getTile(x, y).wallId == wallId) { + results.emplace(x, y); + } + } + } + } + + std::pair parseLookupKey(int lookupKey) + { + int y = lookupKey % world.height; + return {(lookupKey - y) / world.height, y}; + } + + void searchItems(int itemId, std::set> &results) + { + for (auto [lookupKey, chestId] : chestLookup) { + for (const Item &item : world.chests[chestId].items) { + if (item.id == itemId) { + results.insert(parseLookupKey(lookupKey)); + break; + } + } + } + for (auto [lookupKey, entityId] : entityLookup) { + const TileEntity &entity = world.tileEntities[entityId]; + for (const Item &item : entity.items) { + if (item.id == itemId) { + results.insert(parseLookupKey(lookupKey)); + break; + } + } + for (const Item &item : entity.dyes) { + if (item.id == itemId) { + results.insert(parseLookupKey(lookupKey)); + break; + } + } + } + } + + void putImageData(const std::vector &pixels, const char *ctx) + { + EM_ASM( + { + const data = + new Uint8ClampedArray(wasmMemory.buffer, $0, $1 * $2 * 4); + const imageData = new ImageData(data, $1); + self[UTF8ToString($3)].putImageData(imageData, 0, 0); + }, + pixels.data(), + world.width, + world.height, + ctx); + } public: emscripten::val loadWorldFile(const std::string &data) @@ -459,6 +561,7 @@ class Loader } } } + searchResults.clear(); return dumpWorld(world); } @@ -471,17 +574,7 @@ class Loader pixels.push_back(getTileColor(x, y, world).abgr); } } - EM_ASM_( - { - const data = HEAPU8.slice($0, $0 + $1 * $2 * 4); - const ctx = self['ctx']; - const imageData = ctx.getImageData(0, 0, $1, $2); - imageData.data.set(data); - ctx.putImageData(imageData, 0, 0); - }, - pixels.data(), - world.width, - world.height); + putImageData(pixels, "ctx"); } emscripten::val getTile(int x, int y) @@ -507,6 +600,64 @@ class Loader } return result; } + + void search(const emscripten::val &queries) + { + int numQueries = queries["length"].as(); + std::set> results; + std::set blockIds; + for (int i = 0; i < numQueries; ++i) { + int id = queries[i]["id"].as(); + switch (queries[i]["type"].as()) { + case 0: + blockIds.insert(id); + break; + case 1: + searchFramedBlocks( + id, + queries[i]["minU"].as(), + queries[i]["maxU"].as(), + queries[i]["minV"].as(), + queries[i]["maxV"].as(), + results); + break; + case 2: + searchWalls(id, results); + break; + case 3: + searchItems(id, results); + break; + } + } + if (!blockIds.empty()) { + searchBlocks(blockIds, results); + } + searchResults.clear(); + searchResults.assign(results.begin(), results.end()); + + std::vector pixels(world.width * world.height, 0xbf000000); + for (auto [x, y] : searchResults) { + pixels[y * world.width + x] = 0xffffffff; + } + putImageData(pixels, "overlayCtx"); + } + + emscripten::val findNext(int x, int y, int direction) + { + std::pair pt{x, y}; + auto bound = + std::lower_bound(searchResults.begin(), searchResults.end(), pt); + if (direction < 0) { + if (bound == searchResults.begin()) { + return emscripten::val::null(); + } + --bound; + } else if (bound != searchResults.end() && *bound == pt) { + ++bound; + } + return bound == searchResults.end() ? emscripten::val::null() + : marshalData(*bound); + } }; EMSCRIPTEN_BINDINGS(terramap) @@ -515,7 +666,9 @@ EMSCRIPTEN_BINDINGS(terramap) .constructor() .function("loadWorldFile", &Loader::loadWorldFile) .function("renderToCanvas", &Loader::renderToCanvas) - .function("getTile", &Loader::getTile); + .function("getTile", &Loader::getTile) + .function("search", &Loader::search) + .function("findNext", &Loader::findNext); } #else int main() diff --git a/wasm/src/worker.js b/wasm/src/worker.js index af0d408..e403d4c 100644 --- a/wasm/src/worker.js +++ b/wasm/src/worker.js @@ -4,9 +4,15 @@ self.addEventListener('message', (e) => { self.ctx = self.canvas.getContext('2d'); self.ctx.msImageSmoothingEnabled = false; self.ctx.mozImageSmoothingEnabled = false; - self.ctx.msImageSmoothingEnabled = false; self.ctx.imageSmoothingEnabled = false; } + if (e.data.overlayCanvas) { + self.overlayCanvas = e.data.overlayCanvas; + self.overlayCtx = self.overlayCanvas.getContext('2d'); + self.overlayCtx.msImageSmoothingEnabled = false; + self.overlayCtx.mozImageSmoothingEnabled = false; + self.overlayCtx.imageSmoothingEnabled = false; + } if (e.data.file) { self.start(e.data.file); } @@ -16,6 +22,16 @@ self.addEventListener('message', (e) => { if (e.data.selectTile) { onSelectTile(e.data.selectTile); } + if (e.data.search) { + onSearch(e.data.search); + } + if (e.data.clearHighlight) { + const { width, height } = self.overlayCanvas; + self.overlayCtx.clearRect(0, 0, width, height); + } + if (e.data.findNext) { + onFindNext(e.data.findNext); + } }); async function start(file) { @@ -40,9 +56,12 @@ async function start(file) { } self.canvas.width = world.width; self.canvas.height = world.height; + self.overlayCanvas.width = world.width; + self.overlayCanvas.height = world.height; self.postMessage({ status: 'Rendering tiles...', world }); self.terramap.renderToCanvas(); + self.postMessage({ status: 'Done.', done: true }); } function getTileInfo(tile) { @@ -141,3 +160,41 @@ function onSelectTile({ x, y }) { self.postMessage({ tile }); } } + +function onSearch(search) { + const queries = []; + for (const info of search) { + if (info.isTile) { + if (info.parent) { + let sizeX = 1; + let sizeY = 1; + if (info.parent.Size) { + sizeX = info.parent.Size[0] - '0'; + sizeY = info.parent.Size[2] - '0'; + } + queries.push({ + type: 1, + id: Number(info.parent.Id), + minU: info.U, + maxU: info.U + 18 * sizeX, + minV: info.V, + maxV: info.V + 18 * sizeY, + }); + } else { + queries.push({ type: 0, id: Number(info.Id) }); + } + } else if (info.isWall) { + queries.push({ type: 2, id: Number(info.Id) }); + } else if (info.isItem) { + queries.push({ type: 3, id: Number(info.Id) }); + } + } + self.terramap.search(queries); +} + +function onFindNext({ x, y, direction }) { + const pos = self.terramap.findNext(x, y, direction); + if (pos) { + self.postMessage({ select: { x: pos[0], y: pos[1] } }); + } +} From 40f6357cef8ffbd808f3a48b563db8d74f71b9c9 Mon Sep 17 00:00:00 2001 From: Alpha Date: Sun, 10 Aug 2025 02:06:02 -0400 Subject: [PATCH 08/13] Display more tile info --- resources/js/main.js | 28 ++++++++++++++++++++++++++++ resources/js/settings.js | 34 ++++++++++++++++++++++++++++++++++ wasm/src/main.cpp | 12 ++++++------ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/resources/js/main.js b/resources/js/main.js index e9cebdb..4ab3131 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -550,6 +550,31 @@ function drawSelectionIndicator() { selectionCtx.stroke(); } +function getTileInfoList(tile) { + const info = []; + const fields = [ + ['actuated', 'Actuated'], + ['echoCoatBlock', 'Echo Coat Block'], + ['echoCoatWall', 'Echo Coat Wall'], + ['illuminantBlock', 'Illuminant Block'], + ['illuminantWall', 'Illuminant Wall'], + ['liquidAmount', 'Liquid Amount'], + ['slope', 'Slope'], + ]; + for (const [key, label] of fields) { + if (tile[key]) { + info.push(`${label}: ${tile[key]}`); + } + } + if (tile.blockPaint > 0) { + info.push(`Block Paint: ${settings.Paints[tile.blockPaint].Name}`); + } + if (tile.wallPaint > 0) { + info.push(`Wall Paint: ${settings.Paints[tile.wallPaint].Name}`); + } + return info.sort(); +} + function fileNameChanged (evt) { file = evt.target.files[0]; @@ -580,6 +605,9 @@ function onWorldLoaderWorkerMessage(e) { if (e.data.tile) { $("#tileInfoList").empty(); const tile = e.data.tile; + for (const row of getTileInfoList(tile)) { + $("#tileInfoList").append(`
  • ${row}
  • `); + } if (tile.chest) { if (tile.chest.name.length > 0) { tile.text += ` - ${tile.chest.name}`; diff --git a/resources/js/settings.js b/resources/js/settings.js index 3d27bcb..f1fd367 100644 --- a/resources/js/settings.js +++ b/resources/js/settings.js @@ -43473,4 +43473,38 @@ var settings = { Name: "Mythical", }, ], + Paints: [ + { Id: 0, Name: "None" }, + { Id: 1, Name: "Red" }, + { Id: 2, Name: "Orange" }, + { Id: 3, Name: "Yellow" }, + { Id: 4, Name: "Lime" }, + { Id: 5, Name: "Green" }, + { Id: 6, Name: "Teal" }, + { Id: 7, Name: "Cyan" }, + { Id: 8, Name: "Sky Blue" }, + { Id: 9, Name: "Blue" }, + { Id: 10, Name: "Purple" }, + { Id: 11, Name: "Violet" }, + { Id: 12, Name: "Pink" }, + { Id: 13, Name: "Deep Red" }, + { Id: 14, Name: "Deep Orange" }, + { Id: 15, Name: "Deep Yellow" }, + { Id: 16, Name: "Deep Lime" }, + { Id: 17, Name: "Deep Green" }, + { Id: 18, Name: "Deep Teal" }, + { Id: 19, Name: "Deep Cyan" }, + { Id: 20, Name: "Deep Sky Blue" }, + { Id: 21, Name: "Deep Blue" }, + { Id: 22, Name: "Deep Purple" }, + { Id: 23, Name: "Deep Violet" }, + { Id: 24, Name: "Deep Pink" }, + { Id: 25, Name: "Black" }, + { Id: 26, Name: "White" }, + { Id: 27, Name: "Gray" }, + { Id: 28, Name: "Brown" }, + { Id: 29, Name: "Shadow" }, + { Id: 30, Name: "Negative" }, + { Id: 31, Name: "Illuminant Paint" }, + ], }; diff --git a/wasm/src/main.cpp b/wasm/src/main.cpp index d16315f..af53f9b 100644 --- a/wasm/src/main.cpp +++ b/wasm/src/main.cpp @@ -85,17 +85,17 @@ std::string marshalData(Slope slope) { switch (slope) { case Slope::none: - return "none"; + return ""; case Slope::half: - return "half"; + return "Half"; case Slope::topRight: - return "topRight"; + return "Top Right"; case Slope::topLeft: - return "topLeft"; + return "Top Left"; case Slope::bottomRight: - return "bottomRight"; + return "Bottom Right"; case Slope::bottomLeft: - return "bottomLeft"; + return "Bottom Left"; } } From 1fded8bbfee1bcd1d1f1027f60ecaae5ea6243b9 Mon Sep 17 00:00:00 2001 From: Alpha Date: Sun, 10 Aug 2025 02:37:15 -0400 Subject: [PATCH 09/13] Wasm: delete redundant js --- index.html | 1 - resources/js/DataStream.js | 1532 ----------------------------------- resources/js/MapHelper.js | 1085 ------------------------- resources/js/WorldLoader.js | 813 ------------------- 4 files changed, 3431 deletions(-) delete mode 100644 resources/js/DataStream.js delete mode 100644 resources/js/MapHelper.js delete mode 100644 resources/js/WorldLoader.js diff --git a/index.html b/index.html index f73b873..0fa6340 100644 --- a/index.html +++ b/index.html @@ -180,7 +180,6 @@ -