|
|
|
|
@@ -20,10 +20,10 @@
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
constexpr wchar_t kNamespaceTagKey[] = L"weaveloader:id";
|
|
|
|
|
constexpr wchar_t kNamespaceKindTagKey[] = L"weaveloader:kind";
|
|
|
|
|
constexpr char kMissingBlockId[] = "weaveloader:missing_block";
|
|
|
|
|
constexpr char kMissingItemId[] = "weaveloader:missing_item";
|
|
|
|
|
constexpr int kMissingBlockNumericId = 255;
|
|
|
|
|
constexpr int kMissingItemNumericId = 31999;
|
|
|
|
|
constexpr int kMissingBlockVanillaFallbackId = 7; // bedrock
|
|
|
|
|
constexpr int kMissingBlockDescriptionId = 900000;
|
|
|
|
|
constexpr int kMissingItemDescriptionId = 900001;
|
|
|
|
|
constexpr ptrdiff_t kItemIdOffset = 0x20;
|
|
|
|
|
@@ -38,6 +38,7 @@ namespace
|
|
|
|
|
using LevelChunkSetTile_fn = bool (__fastcall *)(void* thisPtr, int x, int y, int z, int tile);
|
|
|
|
|
using LevelChunkGetPos_fn = void* (__fastcall *)(void* thisPtr);
|
|
|
|
|
using LevelChunkGetHighestNonEmptyY_fn = int (__fastcall *)(void* thisPtr);
|
|
|
|
|
using CompressedTileStorageSet_fn = void (__fastcall *)(void* thisPtr, int x, int y, int z, int val);
|
|
|
|
|
|
|
|
|
|
struct CompoundTagLayout
|
|
|
|
|
{
|
|
|
|
|
@@ -55,10 +56,15 @@ namespace
|
|
|
|
|
|
|
|
|
|
std::once_flag s_symbolInitOnce;
|
|
|
|
|
TagNewTag_fn s_tagNewTag = nullptr;
|
|
|
|
|
void* s_tileArray = nullptr;
|
|
|
|
|
LevelChunkGetTile_fn s_levelChunkGetTile = nullptr;
|
|
|
|
|
LevelChunkSetTile_fn s_levelChunkSetTile = nullptr;
|
|
|
|
|
LevelChunkGetPos_fn s_levelChunkGetPos = nullptr;
|
|
|
|
|
LevelChunkGetHighestNonEmptyY_fn s_levelChunkGetHighestNonEmptyY = nullptr;
|
|
|
|
|
CompressedTileStorageSet_fn s_compressedTileStorageSet = nullptr;
|
|
|
|
|
|
|
|
|
|
// LevelChunk layout: vtable(8) + byteArray(16) = 24, then lowerBlocks at 24, upperBlocks at 32
|
|
|
|
|
static constexpr size_t kLevelChunkLowerBlocksOffset = 24;
|
|
|
|
|
bool s_missingPlaceholdersReady = false;
|
|
|
|
|
int s_chunkRemapLogCount = 0;
|
|
|
|
|
int s_chunkSaveLogCount = 0;
|
|
|
|
|
@@ -72,6 +78,8 @@ namespace
|
|
|
|
|
int z = 0;
|
|
|
|
|
};
|
|
|
|
|
std::unordered_map<void*, ChunkMeta> s_chunkMetaByPtr;
|
|
|
|
|
std::unordered_map<void*, std::unordered_map<int, std::string>> s_loadedNamespaceByChunk;
|
|
|
|
|
std::unordered_map<std::string, std::unordered_map<int, std::string>> s_chunkNamespaceCache;
|
|
|
|
|
|
|
|
|
|
struct ConsoleSavePathLayout
|
|
|
|
|
{
|
|
|
|
|
@@ -104,7 +112,6 @@ namespace
|
|
|
|
|
virtual int closeHandle(void* fileEntry) = 0;
|
|
|
|
|
virtual void finalizeWrite() = 0;
|
|
|
|
|
virtual void tick() = 0;
|
|
|
|
|
virtual bool doesFileExist(ConsoleSavePathLayout fileName) = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct RuntimeFileEntryLayout
|
|
|
|
|
@@ -122,17 +129,20 @@ namespace
|
|
|
|
|
return storagePtr && IsReadableRange(storagePtr, sizeof(void*) + sizeof(std::wstring) + sizeof(void*));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::wstring MakeChunkNamespacePath(const McRegionChunkStorageLayout* storage, int chunkX, int chunkZ)
|
|
|
|
|
static std::wstring MakeChunkNamespacePathCoordsOnly(int chunkX, int chunkZ)
|
|
|
|
|
{
|
|
|
|
|
std::wstringstream ss;
|
|
|
|
|
std::hash<std::wstring> hasher;
|
|
|
|
|
const size_t prefixHash = hasher(storage ? storage->prefix : std::wstring());
|
|
|
|
|
ss << L"modloader\\blockns\\"
|
|
|
|
|
<< std::hex << prefixHash << std::dec
|
|
|
|
|
<< L"\\c." << chunkX << L"." << chunkZ << L".txt";
|
|
|
|
|
ss << L"modloader\\blockns_v3\\c." << chunkX << L"." << chunkZ << L".txt";
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string MakeChunkCacheKey(const McRegionChunkStorageLayout* storage, int chunkX, int chunkZ)
|
|
|
|
|
{
|
|
|
|
|
const std::wstring path = MakeChunkNamespacePathCoordsOnly(chunkX, chunkZ);
|
|
|
|
|
std::string key(path.begin(), path.end());
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool SaveReadAllText(void* saveFile, const std::wstring& path, std::string* outText)
|
|
|
|
|
{
|
|
|
|
|
if (!saveFile || !outText || !IsReadableRange(saveFile, sizeof(void*)))
|
|
|
|
|
@@ -145,11 +155,18 @@ namespace
|
|
|
|
|
return false;
|
|
|
|
|
save->setFilePointer(fileEntry, 0, nullptr, FILE_BEGIN);
|
|
|
|
|
|
|
|
|
|
unsigned int fileSize = 0;
|
|
|
|
|
if (IsReadableRange(fileEntry, sizeof(RuntimeFileEntryLayout)))
|
|
|
|
|
if (!IsReadableRange(fileEntry, sizeof(RuntimeFileEntryLayout)))
|
|
|
|
|
{
|
|
|
|
|
const auto* entry = reinterpret_cast<const RuntimeFileEntryLayout*>(fileEntry);
|
|
|
|
|
fileSize = entry->length;
|
|
|
|
|
save->closeHandle(fileEntry);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const auto* entry = reinterpret_cast<const RuntimeFileEntryLayout*>(fileEntry);
|
|
|
|
|
const unsigned int fileSize = entry->length;
|
|
|
|
|
// Hard cap for corrupted metadata; chunk namespace files should stay tiny.
|
|
|
|
|
if (fileSize > (1024u * 1024u))
|
|
|
|
|
{
|
|
|
|
|
save->closeHandle(fileEntry);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string text;
|
|
|
|
|
@@ -171,15 +188,6 @@ namespace
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool SaveFileExists(void* saveFile, const std::wstring& path)
|
|
|
|
|
{
|
|
|
|
|
if (!saveFile || !IsReadableRange(saveFile, sizeof(void*)))
|
|
|
|
|
return false;
|
|
|
|
|
auto* save = reinterpret_cast<RuntimeConsoleSaveFile*>(saveFile);
|
|
|
|
|
ConsoleSavePathLayout savePath(path);
|
|
|
|
|
return save->doesFileExist(savePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool SaveWriteAllText(void* saveFile, const std::wstring& path, const std::string& text)
|
|
|
|
|
{
|
|
|
|
|
if (!saveFile || !IsReadableRange(saveFile, sizeof(void*)))
|
|
|
|
|
@@ -198,6 +206,11 @@ namespace
|
|
|
|
|
return ok && bytesWritten == text.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool WriteChunkNamespaceMap(
|
|
|
|
|
void* saveFile,
|
|
|
|
|
const std::wstring& path,
|
|
|
|
|
const std::unordered_map<int, std::string>& map);
|
|
|
|
|
|
|
|
|
|
static bool ReadChunkNamespaceMap(
|
|
|
|
|
void* saveFile,
|
|
|
|
|
const std::wstring& path,
|
|
|
|
|
@@ -207,9 +220,6 @@ namespace
|
|
|
|
|
return false;
|
|
|
|
|
outMap->clear();
|
|
|
|
|
|
|
|
|
|
if (!SaveFileExists(saveFile, path))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
std::string text;
|
|
|
|
|
if (!SaveReadAllText(saveFile, path, &text))
|
|
|
|
|
return false;
|
|
|
|
|
@@ -244,9 +254,46 @@ namespace
|
|
|
|
|
if (expectedEntries >= 0 && parsedEntries >= expectedEntries)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (expectedEntries < 0)
|
|
|
|
|
{
|
|
|
|
|
// Require explicit count header for safety; legacy files can contain stale tail bytes.
|
|
|
|
|
outMap->clear();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsLikelyNamespacedId(const std::string& s)
|
|
|
|
|
{
|
|
|
|
|
if (s.size() < 3 || s.size() > 200)
|
|
|
|
|
return false;
|
|
|
|
|
if (s.find(':') == std::string::npos)
|
|
|
|
|
return false;
|
|
|
|
|
for (char c : s)
|
|
|
|
|
{
|
|
|
|
|
if (c <= 0x20 || c == '\x7f')
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool ReadChunkNamespaceMapWithFallback(
|
|
|
|
|
const McRegionChunkStorageLayout* storage,
|
|
|
|
|
int chunkX,
|
|
|
|
|
int chunkZ,
|
|
|
|
|
std::unordered_map<int, std::string>* outMap)
|
|
|
|
|
{
|
|
|
|
|
if (!storage || !storage->saveFile || !outMap)
|
|
|
|
|
return false;
|
|
|
|
|
outMap->clear();
|
|
|
|
|
|
|
|
|
|
// Preferred stable path that does not depend on object layout/prefix decoding.
|
|
|
|
|
const std::wstring v3Path = MakeChunkNamespacePathCoordsOnly(chunkX, chunkZ);
|
|
|
|
|
if (ReadChunkNamespaceMap(storage->saveFile, v3Path, outMap) && !outMap->empty())
|
|
|
|
|
return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool WriteChunkNamespaceMap(
|
|
|
|
|
void* saveFile,
|
|
|
|
|
const std::wstring& path,
|
|
|
|
|
@@ -294,6 +341,26 @@ namespace
|
|
|
|
|
meta.z = chunkZ;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void SetLoadedChunkNamespaces(void* chunkPtr, const std::unordered_map<int, std::string>& entries)
|
|
|
|
|
{
|
|
|
|
|
if (!chunkPtr)
|
|
|
|
|
return;
|
|
|
|
|
std::lock_guard<std::mutex> lock(s_chunkMetaMutex);
|
|
|
|
|
s_loadedNamespaceByChunk[chunkPtr] = entries;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool TryGetLoadedChunkNamespaces(void* chunkPtr, std::unordered_map<int, std::string>* outEntries)
|
|
|
|
|
{
|
|
|
|
|
if (!chunkPtr || !outEntries)
|
|
|
|
|
return false;
|
|
|
|
|
std::lock_guard<std::mutex> lock(s_chunkMetaMutex);
|
|
|
|
|
const auto it = s_loadedNamespaceByChunk.find(chunkPtr);
|
|
|
|
|
if (it == s_loadedNamespaceByChunk.end())
|
|
|
|
|
return false;
|
|
|
|
|
*outEntries = it->second;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool TryResolveChunkCoords(void* levelChunkPtr, int* outX, int* outZ)
|
|
|
|
|
{
|
|
|
|
|
if (!levelChunkPtr || !s_levelChunkGetPos)
|
|
|
|
|
@@ -307,11 +374,103 @@ namespace
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsValidRuntimeTileId(int tileId)
|
|
|
|
|
{
|
|
|
|
|
if (tileId == 0)
|
|
|
|
|
return true;
|
|
|
|
|
if (tileId < 0 || tileId > 255)
|
|
|
|
|
return false;
|
|
|
|
|
if (!s_tileArray || !IsReadableRange(s_tileArray, sizeof(void*)))
|
|
|
|
|
return true; // can't validate, don't aggressively rewrite
|
|
|
|
|
|
|
|
|
|
// Tile::tiles is a global Tile** pointer variable in this build.
|
|
|
|
|
// Symbol points at the variable, so dereference once to get the actual table.
|
|
|
|
|
auto** tiles = *reinterpret_cast<void***>(s_tileArray);
|
|
|
|
|
if (!tiles || !IsReadableRange(tiles, sizeof(void*) * 256))
|
|
|
|
|
return true; // can't validate safely
|
|
|
|
|
|
|
|
|
|
return tiles[tileId] != nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsVanillaId(const std::string& namespacedId)
|
|
|
|
|
{
|
|
|
|
|
return namespacedId.rfind("minecraft:", 0) == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int ResolveSafeMissingBlockFallbackId()
|
|
|
|
|
{
|
|
|
|
|
int fallbackId = IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Block);
|
|
|
|
|
if (IsValidRuntimeTileId(fallbackId))
|
|
|
|
|
return fallbackId;
|
|
|
|
|
const int missingBlockId = IdRegistry::Instance().GetNumericId(IdRegistry::Type::Block, kMissingBlockId);
|
|
|
|
|
if (IsValidRuntimeTileId(missingBlockId))
|
|
|
|
|
return missingBlockId;
|
|
|
|
|
if (IsValidRuntimeTileId(kMissingBlockVanillaFallbackId))
|
|
|
|
|
return kMissingBlockVanillaFallbackId;
|
|
|
|
|
if (IsValidRuntimeTileId(1))
|
|
|
|
|
return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsTilePointerValid(int tileId)
|
|
|
|
|
{
|
|
|
|
|
if (tileId == 0)
|
|
|
|
|
return true;
|
|
|
|
|
if (tileId < 0 || tileId > 255)
|
|
|
|
|
return false;
|
|
|
|
|
if (!s_tileArray || !IsReadableRange(s_tileArray, sizeof(void*)))
|
|
|
|
|
return true;
|
|
|
|
|
auto** tiles = *reinterpret_cast<void***>(s_tileArray);
|
|
|
|
|
if (!tiles || !IsReadableRange(tiles, sizeof(void*) * 256))
|
|
|
|
|
return true;
|
|
|
|
|
return tiles[tileId] != nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool SafeSetChunkTile(void* levelChunkPtr, int x, int y, int z, int tileId)
|
|
|
|
|
{
|
|
|
|
|
if (!s_levelChunkSetTile)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
// If the current block has no valid Tile pointer, setTileAndData will crash when it
|
|
|
|
|
// calls Tile::tiles[old]->onRemoving. Clear it to air first via direct block storage write.
|
|
|
|
|
if (s_levelChunkGetTile && s_compressedTileStorageSet && levelChunkPtr && IsReadableRange(levelChunkPtr, kLevelChunkLowerBlocksOffset + 16))
|
|
|
|
|
{
|
|
|
|
|
#if defined(_MSC_VER)
|
|
|
|
|
__try
|
|
|
|
|
{
|
|
|
|
|
int current = s_levelChunkGetTile(levelChunkPtr, x, y, z);
|
|
|
|
|
if (current != 0 && !IsTilePointerValid(current))
|
|
|
|
|
{
|
|
|
|
|
void* blocksPtr = *reinterpret_cast<void**>(static_cast<char*>(levelChunkPtr) + (y >= 128 ? kLevelChunkLowerBlocksOffset + 8 : kLevelChunkLowerBlocksOffset));
|
|
|
|
|
if (blocksPtr && IsReadableRange(blocksPtr, sizeof(void*)))
|
|
|
|
|
{
|
|
|
|
|
s_compressedTileStorageSet(blocksPtr, x, y % 128, z, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
__except (EXCEPTION_EXECUTE_HANDLER)
|
|
|
|
|
{
|
|
|
|
|
// Ignore - we'll try setTile anyway
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if defined(_MSC_VER)
|
|
|
|
|
__try
|
|
|
|
|
{
|
|
|
|
|
s_levelChunkSetTile(levelChunkPtr, x, y, z, tileId);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
__except (EXCEPTION_EXECUTE_HANDLER)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
s_levelChunkSetTile(levelChunkPtr, x, y, z, tileId);
|
|
|
|
|
return true;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string ResolveNamespacedItemId(int numericId)
|
|
|
|
|
{
|
|
|
|
|
std::string namespacedId = IdRegistry::Instance().GetStringId(IdRegistry::Type::Item, numericId);
|
|
|
|
|
@@ -322,7 +481,7 @@ namespace
|
|
|
|
|
return IdRegistry::Instance().GetStringId(IdRegistry::Type::Block, numericId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int ResolveNumericItemId(const std::string& namespacedId, int oldNumericId)
|
|
|
|
|
static int ResolveNumericItemId(const std::string& namespacedId, int oldNumericId, const std::wstring& kind = L"")
|
|
|
|
|
{
|
|
|
|
|
if (namespacedId.empty())
|
|
|
|
|
return -1;
|
|
|
|
|
@@ -345,7 +504,7 @@ namespace
|
|
|
|
|
if (blockId >= 0)
|
|
|
|
|
return blockId;
|
|
|
|
|
|
|
|
|
|
if (wasKnownAsBlock)
|
|
|
|
|
if (wasKnownAsBlock || kind == L"block")
|
|
|
|
|
return IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Block);
|
|
|
|
|
|
|
|
|
|
return IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Item);
|
|
|
|
|
@@ -456,11 +615,18 @@ namespace
|
|
|
|
|
|
|
|
|
|
namespace WorldIdRemap
|
|
|
|
|
{
|
|
|
|
|
static thread_local int s_remapStage = 0;
|
|
|
|
|
|
|
|
|
|
void SetTagNewTagSymbol(void* fnPtr)
|
|
|
|
|
{
|
|
|
|
|
s_tagNewTag = reinterpret_cast<TagNewTag_fn>(fnPtr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetTileArraySymbol(void* tileArrayPtr)
|
|
|
|
|
{
|
|
|
|
|
s_tileArray = tileArrayPtr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetLevelChunkTileSymbols(void* getTileFn, void* setTileFn, void* getPosFn, void* getHighestNonEmptyYFn)
|
|
|
|
|
{
|
|
|
|
|
s_levelChunkGetTile = reinterpret_cast<LevelChunkGetTile_fn>(getTileFn);
|
|
|
|
|
@@ -469,35 +635,51 @@ namespace WorldIdRemap
|
|
|
|
|
s_levelChunkGetHighestNonEmptyY = reinterpret_cast<LevelChunkGetHighestNonEmptyY_fn>(getHighestNonEmptyYFn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetCompressedTileStorageSetSymbol(void* setFn)
|
|
|
|
|
{
|
|
|
|
|
s_compressedTileStorageSet = reinterpret_cast<CompressedTileStorageSet_fn>(setFn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EnsureMissingPlaceholders()
|
|
|
|
|
{
|
|
|
|
|
if (s_missingPlaceholdersReady)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
IdRegistry::Instance().RegisterVanilla(IdRegistry::Type::Block, kMissingBlockNumericId, kMissingBlockId);
|
|
|
|
|
IdRegistry::Instance().RegisterVanilla(IdRegistry::Type::Item, kMissingItemNumericId, kMissingItemId);
|
|
|
|
|
const int missingBlockId = IdRegistry::Instance().Register(IdRegistry::Type::Block, kMissingBlockId);
|
|
|
|
|
const int missingItemId = IdRegistry::Instance().Register(IdRegistry::Type::Item, kMissingItemId);
|
|
|
|
|
|
|
|
|
|
ModStrings::Register(kMissingBlockDescriptionId, L"Missing Block");
|
|
|
|
|
GameObjectFactory::CreateTile(
|
|
|
|
|
kMissingBlockNumericId,
|
|
|
|
|
1,
|
|
|
|
|
1.0f,
|
|
|
|
|
1.0f,
|
|
|
|
|
1,
|
|
|
|
|
L"bedrock",
|
|
|
|
|
0.0f,
|
|
|
|
|
15,
|
|
|
|
|
kMissingBlockDescriptionId);
|
|
|
|
|
IdRegistry::Instance().SetMissingFallback(IdRegistry::Type::Block, kMissingBlockNumericId);
|
|
|
|
|
if (missingBlockId < 0)
|
|
|
|
|
LogUtil::Log("[WeaveLoader] Failed to register missing block placeholder (no free block IDs)");
|
|
|
|
|
if (missingItemId < 0)
|
|
|
|
|
LogUtil::Log("[WeaveLoader] Failed to register missing item placeholder (no free item IDs)");
|
|
|
|
|
|
|
|
|
|
ModStrings::Register(kMissingItemDescriptionId, L"Missing Item");
|
|
|
|
|
GameObjectFactory::CreateItem(
|
|
|
|
|
kMissingItemNumericId,
|
|
|
|
|
64,
|
|
|
|
|
0,
|
|
|
|
|
L"apple",
|
|
|
|
|
kMissingItemDescriptionId);
|
|
|
|
|
IdRegistry::Instance().SetMissingFallback(IdRegistry::Type::Item, kMissingItemNumericId);
|
|
|
|
|
if (missingBlockId >= 0)
|
|
|
|
|
{
|
|
|
|
|
ModStrings::Register(kMissingBlockDescriptionId, L"Missing Block");
|
|
|
|
|
GameObjectFactory::CreateTile(
|
|
|
|
|
missingBlockId,
|
|
|
|
|
1,
|
|
|
|
|
1.0f,
|
|
|
|
|
1.0f,
|
|
|
|
|
1,
|
|
|
|
|
L"bedrock",
|
|
|
|
|
0.0f,
|
|
|
|
|
15,
|
|
|
|
|
kMissingBlockDescriptionId);
|
|
|
|
|
IdRegistry::Instance().SetMissingFallback(IdRegistry::Type::Block, missingBlockId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (missingItemId >= 0)
|
|
|
|
|
{
|
|
|
|
|
ModStrings::Register(kMissingItemDescriptionId, L"Missing Item");
|
|
|
|
|
GameObjectFactory::CreateItem(
|
|
|
|
|
missingItemId,
|
|
|
|
|
64,
|
|
|
|
|
0,
|
|
|
|
|
L"apple",
|
|
|
|
|
kMissingItemDescriptionId);
|
|
|
|
|
IdRegistry::Instance().SetMissingFallback(IdRegistry::Type::Item, missingItemId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_missingPlaceholdersReady = true;
|
|
|
|
|
LogUtil::Log("[WeaveLoader] Missing content placeholders ready: block=%d item=%d",
|
|
|
|
|
@@ -505,8 +687,9 @@ namespace WorldIdRemap
|
|
|
|
|
IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int RemapChunkBlockIds(void* chunkStoragePtr, void* levelChunkPtr, int chunkX, int chunkZ)
|
|
|
|
|
static int RemapChunkBlockIdsImpl(void* chunkStoragePtr, void* levelChunkPtr, int chunkX, int chunkZ)
|
|
|
|
|
{
|
|
|
|
|
s_remapStage = 1;
|
|
|
|
|
if (!chunkStoragePtr || !levelChunkPtr || !s_levelChunkGetTile || !s_levelChunkSetTile)
|
|
|
|
|
return 0;
|
|
|
|
|
if (!IsValidChunkStorage(chunkStoragePtr))
|
|
|
|
|
@@ -519,37 +702,87 @@ namespace WorldIdRemap
|
|
|
|
|
SetChunkMeta(levelChunkPtr, chunkStoragePtr, chunkX, chunkZ);
|
|
|
|
|
|
|
|
|
|
std::unordered_map<int, std::string> entries;
|
|
|
|
|
const std::wstring path = MakeChunkNamespacePath(storage, chunkX, chunkZ);
|
|
|
|
|
if (!SaveFileExists(storage->saveFile, path))
|
|
|
|
|
return 0;
|
|
|
|
|
const std::string cacheKey = MakeChunkCacheKey(storage, chunkX, chunkZ);
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(s_chunkMetaMutex);
|
|
|
|
|
const auto cacheIt = s_chunkNamespaceCache.find(cacheKey);
|
|
|
|
|
if (cacheIt != s_chunkNamespaceCache.end())
|
|
|
|
|
entries = cacheIt->second;
|
|
|
|
|
}
|
|
|
|
|
if (s_chunkIoLogCount < 32)
|
|
|
|
|
{
|
|
|
|
|
++s_chunkIoLogCount;
|
|
|
|
|
LogUtil::Log("[WeaveLoader] WorldIdRemap blocks: reading chunk namespace map x=%d z=%d", chunkX, chunkZ);
|
|
|
|
|
}
|
|
|
|
|
if (!ReadChunkNamespaceMap(storage->saveFile, path, &entries) || entries.empty())
|
|
|
|
|
return 0;
|
|
|
|
|
if (entries.empty())
|
|
|
|
|
{
|
|
|
|
|
s_remapStage = 2;
|
|
|
|
|
if (ReadChunkNamespaceMapWithFallback(storage, chunkX, chunkZ, &entries) && !entries.empty())
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(s_chunkMetaMutex);
|
|
|
|
|
s_chunkNamespaceCache[cacheKey] = entries;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!entries.empty())
|
|
|
|
|
SetLoadedChunkNamespaces(levelChunkPtr, entries);
|
|
|
|
|
|
|
|
|
|
const int sanitizeFallbackId = ResolveSafeMissingBlockFallbackId();
|
|
|
|
|
int applyMaxY = 127;
|
|
|
|
|
if (s_levelChunkGetHighestNonEmptyY)
|
|
|
|
|
{
|
|
|
|
|
const int highestY = s_levelChunkGetHighestNonEmptyY(levelChunkPtr);
|
|
|
|
|
if (highestY >= 0 && highestY < kChunkMaxY)
|
|
|
|
|
{
|
|
|
|
|
int bound = highestY + 16;
|
|
|
|
|
if (bound >= kChunkMaxY)
|
|
|
|
|
bound = kChunkMaxY - 1;
|
|
|
|
|
applyMaxY = bound;
|
|
|
|
|
}
|
|
|
|
|
else if (highestY >= 0)
|
|
|
|
|
{
|
|
|
|
|
applyMaxY = kChunkMaxY - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (applyMaxY > 127)
|
|
|
|
|
applyMaxY = 127;
|
|
|
|
|
|
|
|
|
|
const int missingBlockFallback = IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Block);
|
|
|
|
|
int changed = 0;
|
|
|
|
|
int skippedInvalid = 0;
|
|
|
|
|
int writeExceptions = 0;
|
|
|
|
|
s_remapStage = 3;
|
|
|
|
|
for (const auto& entry : entries)
|
|
|
|
|
{
|
|
|
|
|
int x = 0;
|
|
|
|
|
int y = 0;
|
|
|
|
|
int z = 0;
|
|
|
|
|
DecodeBlockIndex(entry.first, &x, &y, &z);
|
|
|
|
|
if (y < 0 || y > applyMaxY)
|
|
|
|
|
{
|
|
|
|
|
++skippedInvalid;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!IsLikelyNamespacedId(entry.second))
|
|
|
|
|
{
|
|
|
|
|
++skippedInvalid;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
int newId = IdRegistry::Instance().GetNumericId(IdRegistry::Type::Block, entry.second);
|
|
|
|
|
if (newId < 0)
|
|
|
|
|
newId = missingBlockFallback;
|
|
|
|
|
newId = sanitizeFallbackId;
|
|
|
|
|
if (!IsValidRuntimeTileId(newId))
|
|
|
|
|
newId = sanitizeFallbackId;
|
|
|
|
|
if (newId < 0 || newId > 255)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const int oldId = s_levelChunkGetTile(levelChunkPtr, x, y, z);
|
|
|
|
|
if (newId != oldId)
|
|
|
|
|
{
|
|
|
|
|
s_levelChunkSetTile(levelChunkPtr, x, y, z, newId);
|
|
|
|
|
++changed;
|
|
|
|
|
++skippedInvalid;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!SafeSetChunkTile(levelChunkPtr, x, y, z, newId))
|
|
|
|
|
{
|
|
|
|
|
++writeExceptions;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
++changed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (changed > 0 && s_chunkRemapLogCount < 64)
|
|
|
|
|
@@ -557,9 +790,37 @@ namespace WorldIdRemap
|
|
|
|
|
++s_chunkRemapLogCount;
|
|
|
|
|
LogUtil::Log("[WeaveLoader] WorldIdRemap blocks: remapped %d block ids in chunk", changed);
|
|
|
|
|
}
|
|
|
|
|
if (skippedInvalid > 0 && s_chunkRemapLogCount < 64)
|
|
|
|
|
{
|
|
|
|
|
++s_chunkRemapLogCount;
|
|
|
|
|
LogUtil::Log("[WeaveLoader] WorldIdRemap blocks: skipped %d invalid namespace entries in chunk", skippedInvalid);
|
|
|
|
|
}
|
|
|
|
|
if (writeExceptions > 0 && s_chunkRemapLogCount < 64)
|
|
|
|
|
{
|
|
|
|
|
++s_chunkRemapLogCount;
|
|
|
|
|
LogUtil::Log("[WeaveLoader] WorldIdRemap blocks: skipped %d writes due to setTile exceptions", writeExceptions);
|
|
|
|
|
}
|
|
|
|
|
s_remapStage = 0;
|
|
|
|
|
return changed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int RemapChunkBlockIds(void* chunkStoragePtr, void* levelChunkPtr, int chunkX, int chunkZ)
|
|
|
|
|
{
|
|
|
|
|
#if defined(_MSC_VER)
|
|
|
|
|
__try
|
|
|
|
|
{
|
|
|
|
|
return RemapChunkBlockIdsImpl(chunkStoragePtr, levelChunkPtr, chunkX, chunkZ);
|
|
|
|
|
}
|
|
|
|
|
__except(EXCEPTION_EXECUTE_HANDLER)
|
|
|
|
|
{
|
|
|
|
|
LogUtil::Log("[WeaveLoader] WorldIdRemap blocks: exception while remapping chunk x=%d z=%d stage=%d (skipped)", chunkX, chunkZ, s_remapStage);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
return RemapChunkBlockIdsImpl(chunkStoragePtr, levelChunkPtr, chunkX, chunkZ);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SaveChunkBlockNamespaces(void* chunkStoragePtr, void* levelChunkPtr)
|
|
|
|
|
{
|
|
|
|
|
if (!chunkStoragePtr || !levelChunkPtr || !s_levelChunkGetTile)
|
|
|
|
|
@@ -580,18 +841,18 @@ namespace WorldIdRemap
|
|
|
|
|
SetChunkMeta(levelChunkPtr, chunkStoragePtr, meta.x, meta.z);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::wstring path = MakeChunkNamespacePath(storage, meta.x, meta.z);
|
|
|
|
|
const bool hadExistingMap = SaveFileExists(storage->saveFile, path);
|
|
|
|
|
std::unordered_map<int, std::string> previousEntries;
|
|
|
|
|
if (hadExistingMap)
|
|
|
|
|
ReadChunkNamespaceMap(storage->saveFile, path, &previousEntries);
|
|
|
|
|
const std::wstring path = MakeChunkNamespacePathCoordsOnly(meta.x, meta.z);
|
|
|
|
|
const std::string cacheKey = MakeChunkCacheKey(storage, meta.x, meta.z);
|
|
|
|
|
std::unordered_map<int, std::string> loadedEntries;
|
|
|
|
|
const bool hadLoadedMap = TryGetLoadedChunkNamespaces(levelChunkPtr, &loadedEntries) && !loadedEntries.empty();
|
|
|
|
|
|
|
|
|
|
std::unordered_map<int, std::string> nextEntries;
|
|
|
|
|
const int missingBlockFallback = IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Block);
|
|
|
|
|
int maxY = kChunkMaxY - 1;
|
|
|
|
|
if (s_levelChunkGetHighestNonEmptyY)
|
|
|
|
|
{
|
|
|
|
|
const int highestY = s_levelChunkGetHighestNonEmptyY(levelChunkPtr);
|
|
|
|
|
if (highestY < 0 && !hadExistingMap)
|
|
|
|
|
if (highestY < 0 && !hadLoadedMap)
|
|
|
|
|
return;
|
|
|
|
|
if (highestY >= 0 && highestY < kChunkMaxY)
|
|
|
|
|
maxY = highestY;
|
|
|
|
|
@@ -604,18 +865,32 @@ namespace WorldIdRemap
|
|
|
|
|
for (int x = 0; x < kChunkWidth; ++x)
|
|
|
|
|
{
|
|
|
|
|
const int blockId = s_levelChunkGetTile(levelChunkPtr, x, y, z);
|
|
|
|
|
const int blockIndex = MakeBlockIndex(x, y, z);
|
|
|
|
|
|
|
|
|
|
if (blockId == missingBlockFallback)
|
|
|
|
|
{
|
|
|
|
|
const auto loadedIt = loadedEntries.find(blockIndex);
|
|
|
|
|
if (loadedIt != loadedEntries.end() && !loadedIt->second.empty() && loadedIt->second != kMissingBlockId)
|
|
|
|
|
{
|
|
|
|
|
nextEntries[blockIndex] = loadedIt->second;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string namespacedId = IdRegistry::Instance().GetStringId(IdRegistry::Type::Block, blockId);
|
|
|
|
|
if (namespacedId.empty())
|
|
|
|
|
continue;
|
|
|
|
|
if (IsVanillaId(namespacedId))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const int blockIndex = MakeBlockIndex(x, y, z);
|
|
|
|
|
if (namespacedId == kMissingBlockId)
|
|
|
|
|
{
|
|
|
|
|
const auto it = previousEntries.find(blockIndex);
|
|
|
|
|
if (it != previousEntries.end() && !it->second.empty() && it->second != kMissingBlockId)
|
|
|
|
|
nextEntries[blockIndex] = it->second;
|
|
|
|
|
const auto loadedIt = loadedEntries.find(blockIndex);
|
|
|
|
|
if (loadedIt != loadedEntries.end() && !loadedIt->second.empty() && loadedIt->second != kMissingBlockId)
|
|
|
|
|
{
|
|
|
|
|
nextEntries[blockIndex] = loadedIt->second;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -624,10 +899,14 @@ namespace WorldIdRemap
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextEntries.empty() && !hadExistingMap)
|
|
|
|
|
if (nextEntries.empty() && !hadLoadedMap)
|
|
|
|
|
return;
|
|
|
|
|
if (!WriteChunkNamespaceMap(storage->saveFile, path, nextEntries))
|
|
|
|
|
return;
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(s_chunkMetaMutex);
|
|
|
|
|
s_chunkNamespaceCache[cacheKey] = nextEntries;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (s_chunkSaveLogCount < 64)
|
|
|
|
|
{
|
|
|
|
|
@@ -650,6 +929,8 @@ namespace WorldIdRemap
|
|
|
|
|
const std::string namespacedId = ResolveNamespacedItemId(itemId);
|
|
|
|
|
if (namespacedId.empty() || IsVanillaId(namespacedId))
|
|
|
|
|
return;
|
|
|
|
|
const bool isBlockBacked = !IdRegistry::Instance().GetStringId(IdRegistry::Type::Block, itemId).empty() &&
|
|
|
|
|
IdRegistry::Instance().GetStringId(IdRegistry::Type::Item, itemId).empty();
|
|
|
|
|
|
|
|
|
|
CompoundTagLayout* itemTag = EnsureItemTag(itemInstancePtr);
|
|
|
|
|
if (!itemTag)
|
|
|
|
|
@@ -666,6 +947,7 @@ namespace WorldIdRemap
|
|
|
|
|
const std::wstring namespacedWide(namespacedId.begin(), namespacedId.end());
|
|
|
|
|
if (!PutCompoundString(itemTag, kNamespaceTagKey, namespacedWide))
|
|
|
|
|
return;
|
|
|
|
|
(void)PutCompoundString(itemTag, kNamespaceKindTagKey, isBlockBacked ? L"block" : L"item");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RemapItemInstanceFromTag(void* itemInstancePtr, void* compoundTagPtr)
|
|
|
|
|
@@ -685,9 +967,11 @@ namespace WorldIdRemap
|
|
|
|
|
std::wstring namespacedWide;
|
|
|
|
|
if (!TryGetCompoundString(itemTag, kNamespaceTagKey, &namespacedWide) || namespacedWide.empty())
|
|
|
|
|
return;
|
|
|
|
|
std::wstring kindWide;
|
|
|
|
|
(void)TryGetCompoundString(itemTag, kNamespaceKindTagKey, &kindWide);
|
|
|
|
|
|
|
|
|
|
const std::string namespacedId(namespacedWide.begin(), namespacedWide.end());
|
|
|
|
|
int numericId = ResolveNumericItemId(namespacedId, oldItemId);
|
|
|
|
|
int numericId = ResolveNumericItemId(namespacedId, oldItemId, kindWide);
|
|
|
|
|
if (numericId >= 0)
|
|
|
|
|
*reinterpret_cast<int*>(static_cast<char*>(itemInstancePtr) + kItemIdOffset) = numericId;
|
|
|
|
|
}
|
|
|
|
|
|