feat(blockstates): rotation profiles and placement tracking

This commit is contained in:
Jacobwasbeast
2026-03-11 19:56:21 -05:00
parent b47e3d2354
commit d4f7603390
15 changed files with 840 additions and 26 deletions

View File

@@ -244,6 +244,8 @@ public class ExampleMod : IMod
.Resistance(5f)
.Sound(SoundType.Wood)
.Model("examplemod:block/ruby_chair")
.BlockState("examplemod:ruby_chair")
.RotationProfile(BlockRotationProfile.Facing)
.Name(Text.Translatable("block.examplemod.ruby_chair"))
.InCreativeTab(CreativeTab.Decoration));

View File

@@ -0,0 +1,19 @@
{
"variants": {
"facing=north": {
"model": "examplemod:block/ruby_chair"
},
"facing=east": {
"model": "examplemod:block/ruby_chair",
"y": 90
},
"facing=south": {
"model": "examplemod:block/ruby_chair",
"y": 180
},
"facing=west": {
"model": "examplemod:block/ruby_chair",
"y": 270
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace WeaveLoader.API.Assets;
@@ -43,6 +44,82 @@ internal static class ModelResolver
}
}
internal static void ApplyBlockStates(Identifier id, Block.BlockProperties properties)
{
if (string.IsNullOrWhiteSpace(ModContext.ModFolder))
return;
if (!TryGetBlockStateFilePath(id, properties.BlockStateValue, out string blockStatePath))
return;
if (!File.Exists(blockStatePath))
return;
try
{
using var doc = JsonDocument.Parse(File.ReadAllText(blockStatePath));
if (!doc.RootElement.TryGetProperty("variants", out JsonElement variants) ||
variants.ValueKind != JsonValueKind.Object)
{
return;
}
var variantProps = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var variant in variants.EnumerateObject())
{
string key = NormalizeVariantKey(variant.Name);
CollectVariantProps(variant.Name, variantProps);
JsonElement entry = variant.Value;
if (entry.ValueKind == JsonValueKind.Array)
{
if (entry.GetArrayLength() == 0)
continue;
entry = entry[0];
}
if (entry.ValueKind != JsonValueKind.Object)
continue;
if (!entry.TryGetProperty("model", out JsonElement modelEl) ||
modelEl.ValueKind != JsonValueKind.String)
continue;
string? modelName = modelEl.GetString();
if (string.IsNullOrWhiteSpace(modelName))
continue;
int xRot = 0;
int yRot = 0;
if (entry.TryGetProperty("x", out JsonElement xEl) && xEl.ValueKind == JsonValueKind.Number)
xRot = xEl.GetInt32();
if (entry.TryGetProperty("y", out JsonElement yEl) && yEl.ValueKind == JsonValueKind.Number)
yRot = yEl.GetInt32();
if (!TryLoadModelFromValue(id, modelName!, ModelKind.Block, out ModelData? model) || model == null)
continue;
if (string.IsNullOrWhiteSpace(properties.IconValue) && !string.IsNullOrWhiteSpace(model.IconName))
properties.IconValue = model.IconName;
var boxes = model.Boxes.Count > 0
? RotateBoxes(model.Boxes, xRot, yRot)
: new List<ModelBox>();
properties.ModelVariants ??= new Dictionary<string, List<ModelBox>>(StringComparer.OrdinalIgnoreCase);
properties.ModelVariants[key] = boxes;
}
if (properties.RotationProfileValue == Block.BlockRotationProfile.None)
{
var inferred = InferRotationProfile(variantProps, blockStatePath);
if (inferred != Block.BlockRotationProfile.None)
properties.RotationProfileValue = inferred;
}
}
catch (Exception ex)
{
Logger.Warning($"Failed to parse blockstate JSON '{blockStatePath}': {ex.Message}");
}
}
internal static void ApplyItemModel(Identifier id, Item.ItemProperties properties)
{
if (!ShouldResolveModel(properties.ModelValue, properties.IconValue, ModelKind.Item))
@@ -130,6 +207,11 @@ internal static class ModelResolver
return !string.IsNullOrWhiteSpace(data.IconName) || data.Boxes.Count > 0;
}
private static bool TryLoadModelFromValue(Identifier id, string modelValue, ModelKind kind, out ModelData? model)
{
return TryLoadModel(id, modelValue, kind, out model);
}
private static bool TryGetModelFilePath(Identifier id, string? modelValue, ModelKind kind, out string modelPath, out string modelNamespace)
{
modelPath = "";
@@ -172,6 +254,81 @@ internal static class ModelResolver
return true;
}
private static bool TryGetBlockStateFilePath(Identifier id, string? blockStateValue, out string blockStatePath)
{
blockStatePath = "";
string ns = id.Namespace;
string rel = id.Path;
if (!string.IsNullOrWhiteSpace(blockStateValue))
{
string raw = blockStateValue!;
int colon = raw.IndexOf(':');
if (colon >= 0)
{
ns = raw[..colon];
rel = raw[(colon + 1)..];
}
else
{
rel = raw;
}
}
rel = rel.Replace('\\', '/');
if (rel.StartsWith("blockstates/", StringComparison.OrdinalIgnoreCase))
rel = rel["blockstates/".Length..];
string modRoot = ModContext.ModFolder ?? "";
if (string.IsNullOrWhiteSpace(modRoot))
return false;
blockStatePath = Path.Combine(modRoot, "assets", ns, "blockstates", rel.Replace('/', Path.DirectorySeparatorChar) + ".json");
return true;
}
private static void CollectVariantProps(string rawKey, HashSet<string> props)
{
if (string.IsNullOrWhiteSpace(rawKey))
return;
foreach (string part in rawKey.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
int eq = part.IndexOf('=');
if (eq <= 0)
continue;
props.Add(part[..eq]);
}
}
private static Block.BlockRotationProfile InferRotationProfile(HashSet<string> props, string blockStatePath)
{
bool hasFacing = props.Contains("facing");
bool hasHalf = props.Contains("half");
bool hasOpen = props.Contains("open");
bool hasHinge = props.Contains("hinge");
bool hasRotation = props.Contains("rotation");
if (hasFacing && hasHalf && hasOpen && hasHinge)
return Block.BlockRotationProfile.Door;
if (hasFacing && hasHalf && hasOpen)
return Block.BlockRotationProfile.Trapdoor;
if (hasRotation)
return Block.BlockRotationProfile.StandingSign;
if (hasFacing)
return Block.BlockRotationProfile.Facing;
string file = Path.GetFileNameWithoutExtension(blockStatePath);
if (file.EndsWith("wall_sign", StringComparison.OrdinalIgnoreCase) ||
file.EndsWith("wall_hanging_sign", StringComparison.OrdinalIgnoreCase))
return Block.BlockRotationProfile.WallSign;
if (file.EndsWith("sign", StringComparison.OrdinalIgnoreCase) ||
file.EndsWith("hanging_sign", StringComparison.OrdinalIgnoreCase))
return Block.BlockRotationProfile.StandingSign;
return Block.BlockRotationProfile.None;
}
private static bool TryParseTextures(string modelPath, out Dictionary<string, string> textures)
{
textures = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -266,6 +423,115 @@ internal static class ModelResolver
box.X1 >= 1.0f - eps && box.Y1 >= 1.0f - eps && box.Z1 >= 1.0f - eps;
}
private static string NormalizeVariantKey(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
return "";
string[] parts = raw.Split(',', StringSplitOptions.RemoveEmptyEntries);
var pairs = new List<(string key, string value)>();
foreach (string part in parts)
{
int eq = part.IndexOf('=');
if (eq <= 0 || eq >= part.Length - 1)
continue;
string key = part[..eq].Trim().ToLowerInvariant();
string value = part[(eq + 1)..].Trim().ToLowerInvariant();
if (key.Length == 0 || value.Length == 0)
continue;
pairs.Add((key, value));
}
if (pairs.Count == 0)
return "";
pairs.Sort((a, b) => string.CompareOrdinal(a.key, b.key));
return string.Join(",", pairs.Select(p => $"{p.key}={p.value}"));
}
private static List<ModelBox> RotateBoxes(List<ModelBox> boxes, int xRot, int yRot)
{
int xr = ((xRot % 360) + 360) % 360;
int yr = ((yRot % 360) + 360) % 360;
if (xr == 0 && yr == 0)
return new List<ModelBox>(boxes);
var result = new List<ModelBox>(boxes.Count);
foreach (var box in boxes)
{
float x0 = MathF.Min(box.X0, box.X1);
float y0 = MathF.Min(box.Y0, box.Y1);
float z0 = MathF.Min(box.Z0, box.Z1);
float x1 = MathF.Max(box.X0, box.X1);
float y1 = MathF.Max(box.Y0, box.Y1);
float z1 = MathF.Max(box.Z0, box.Z1);
Span<(float x, float y, float z)> pts = stackalloc (float, float, float)[8];
int i = 0;
for (int xi = 0; xi < 2; xi++)
for (int yi = 0; yi < 2; yi++)
for (int zi = 0; zi < 2; zi++)
{
float x = xi == 0 ? x0 : x1;
float y = yi == 0 ? y0 : y1;
float z = zi == 0 ? z0 : z1;
(x, y, z) = RotatePoint(x, y, z, xr, yr);
pts[i++] = (x, y, z);
}
float rx0 = pts[0].x, ry0 = pts[0].y, rz0 = pts[0].z;
float rx1 = pts[0].x, ry1 = pts[0].y, rz1 = pts[0].z;
for (int j = 1; j < pts.Length; j++)
{
var p = pts[j];
if (p.x < rx0) rx0 = p.x;
if (p.y < ry0) ry0 = p.y;
if (p.z < rz0) rz0 = p.z;
if (p.x > rx1) rx1 = p.x;
if (p.y > ry1) ry1 = p.y;
if (p.z > rz1) rz1 = p.z;
}
result.Add(new ModelBox { X0 = rx0, Y0 = ry0, Z0 = rz0, X1 = rx1, Y1 = ry1, Z1 = rz1 });
}
return result;
}
private static (float x, float y, float z) RotatePoint(float x, float y, float z, int xRot, int yRot)
{
float px = x;
float py = y;
float pz = z;
switch (xRot)
{
case 90:
(py, pz) = (1.0f - pz, py);
break;
case 180:
py = 1.0f - py;
pz = 1.0f - pz;
break;
case 270:
(py, pz) = (pz, 1.0f - py);
break;
}
switch (yRot)
{
case 90:
(px, pz) = (pz, 1.0f - px);
break;
case 180:
px = 1.0f - px;
pz = 1.0f - pz;
break;
case 270:
(px, pz) = (1.0f - pz, px);
break;
}
return (px, py, pz);
}
private static string? SelectTexture(Dictionary<string, string> textures, ModelKind kind)
{
string[] keys = kind == ModelKind.Item

View File

@@ -52,6 +52,19 @@ public enum SoundType
Snow = 9
}
/// <summary>
/// Rotation/profile mapping used when resolving blockstate variants.
/// </summary>
public enum BlockRotationProfile
{
None = 0,
Facing = 1,
WallSign = 2,
StandingSign = 3,
Trapdoor = 4,
Door = 5
}
/// <summary>
/// Fluent builder for defining block properties.
/// </summary>
@@ -73,7 +86,10 @@ public class BlockProperties
internal ToolType RequiredToolValue = ToolType.None;
internal bool AcceptsRedstonePowerValue;
internal List<Assets.ModelBox>? ModelBoxes;
internal Dictionary<string, List<Assets.ModelBox>>? ModelVariants;
internal bool ModelIsFullCube;
internal BlockRotationProfile RotationProfileValue = BlockRotationProfile.None;
internal string? BlockStateValue;
public BlockProperties Material(MaterialType material) { MaterialValue = material; return this; }
public BlockProperties Hardness(float hardness) { HardnessValue = hardness; return this; }
@@ -106,4 +122,8 @@ public class BlockProperties
public BlockProperties RequiredTool(ToolType tool) { RequiredToolValue = tool; return this; }
/// <summary>Marks the block as one that can receive redstone power. Stored for future block callbacks.</summary>
public BlockProperties AcceptsRedstonePower(bool accepts = true) { AcceptsRedstonePowerValue = accepts; return this; }
/// <summary>Optional blockstate JSON name (e.g. "examplemod:ruby_chair").</summary>
public BlockProperties BlockState(string blockStateName) { BlockStateValue = blockStateName; return this; }
/// <summary>Rotation/profile mapping for blockstate variants.</summary>
public BlockProperties RotationProfile(BlockRotationProfile profile) { RotationProfileValue = profile; return this; }
}

View File

@@ -51,6 +51,7 @@ public static class BlockRegistry
public static RegisteredBlock Register(Identifier id, BlockProperties properties)
{
Assets.ModelResolver.ApplyBlockModel(id, properties);
Assets.ModelResolver.ApplyBlockStates(id, properties);
ApplyModelLightDefaults(properties);
int numericId = NativeInterop.native_register_block(
id.ToString(),
@@ -72,10 +73,7 @@ public static class BlockRegistry
throw new InvalidOperationException($"Failed to register block '{id}'. No free IDs or invalid parameters.");
}
if (properties.ModelBoxes != null && properties.ModelBoxes.Count > 0)
{
NativeInterop.native_register_block_model(numericId, properties.ModelBoxes.ToArray(), properties.ModelBoxes.Count);
}
RegisterBlockModels(numericId, properties);
AddToCreative(id, numericId, properties);
@@ -96,6 +94,7 @@ public static class BlockRegistry
return RegisterFalling(id, managedBlock, properties);
Assets.ModelResolver.ApplyBlockModel(id, properties);
Assets.ModelResolver.ApplyBlockStates(id, properties);
ApplyModelLightDefaults(properties);
int numericId = NativeInterop.native_register_managed_block(
id.ToString(),
@@ -117,10 +116,7 @@ public static class BlockRegistry
throw new InvalidOperationException($"Failed to register managed block '{id}'.");
}
if (properties.ModelBoxes != null && properties.ModelBoxes.Count > 0)
{
NativeInterop.native_register_block_model(numericId, properties.ModelBoxes.ToArray(), properties.ModelBoxes.Count);
}
RegisterBlockModels(numericId, properties);
AddToCreative(id, numericId, properties);
@@ -149,6 +145,7 @@ public static class BlockRegistry
private static RegisteredBlock RegisterFalling(Identifier id, Block? managedBlock, BlockProperties properties)
{
Assets.ModelResolver.ApplyBlockModel(id, properties);
Assets.ModelResolver.ApplyBlockStates(id, properties);
ApplyModelLightDefaults(properties);
int numericId = NativeInterop.native_register_falling_block(
id.ToString(),
@@ -170,10 +167,7 @@ public static class BlockRegistry
throw new InvalidOperationException($"Failed to register falling block '{id}'.");
}
if (properties.ModelBoxes != null && properties.ModelBoxes.Count > 0)
{
NativeInterop.native_register_block_model(numericId, properties.ModelBoxes.ToArray(), properties.ModelBoxes.Count);
}
RegisterBlockModels(numericId, properties);
AddToCreative(id, numericId, properties);
@@ -202,6 +196,7 @@ public static class BlockRegistry
public static RegisteredSlabBlock RegisterSlab(Identifier id, BlockProperties properties)
{
Assets.ModelResolver.ApplyBlockModel(id, properties);
Assets.ModelResolver.ApplyBlockStates(id, properties);
ApplyModelLightDefaults(properties);
Identifier doubleId = new($"{id}_double");
int numericId = NativeInterop.native_register_slab_block(
@@ -230,10 +225,7 @@ public static class BlockRegistry
throw new InvalidOperationException($"Failed to resolve generated slab pair '{doubleId}'.");
}
if (properties.ModelBoxes != null && properties.ModelBoxes.Count > 0)
{
NativeInterop.native_register_block_model(numericId, properties.ModelBoxes.ToArray(), properties.ModelBoxes.Count);
}
RegisterBlockModels(numericId, properties);
AddToCreative(id, numericId, properties);
@@ -257,6 +249,27 @@ public static class BlockRegistry
if (!properties.ModelIsFullCube)
properties.LightBlockValue = 0;
}
private static void RegisterBlockModels(int numericId, BlockProperties properties)
{
if (properties.ModelBoxes != null && properties.ModelBoxes.Count > 0)
{
NativeInterop.native_register_block_model(numericId, properties.ModelBoxes.ToArray(), properties.ModelBoxes.Count);
}
if (properties.ModelVariants != null)
{
foreach (var variant in properties.ModelVariants)
{
var boxes = variant.Value;
if (boxes.Count == 0)
continue;
NativeInterop.native_register_block_model_variant(numericId, variant.Key, boxes.ToArray(), boxes.Count);
}
}
NativeInterop.native_register_block_rotation_profile(numericId, (int)properties.RotationProfileValue);
}
internal static bool TryGetIdentifier(int numericId, out Identifier id)
{
lock (s_lock)

View File

@@ -77,6 +77,18 @@ internal static class NativeInterop
[In] Assets.ModelBox[] boxes,
int count);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_register_block_model_variant(
int blockId,
string key,
[In] Assets.ModelBox[] boxes,
int count);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern void native_register_block_rotation_profile(
int blockId,
int profile);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_item(
string namespacedId,

View File

@@ -19,6 +19,7 @@
#include <cstring>
#include <cwchar>
#include <cwctype>
#include <algorithm>
#include <vector>
#include <cctype>
#include <memory>
@@ -33,6 +34,7 @@
namespace GameHooks
{
static bool IsReadableRange(const void* ptr, size_t bytes);
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
@@ -56,6 +58,7 @@ namespace GameHooks
TextureGetSourceDim_fn Original_ClockTextureGetSourceWidth = nullptr;
TextureGetSourceDim_fn Original_ClockTextureGetSourceHeight = nullptr;
ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock = nullptr;
ItemInstanceUseOn_fn Original_ItemInstanceUseOn = nullptr;
ItemInstanceSave_fn Original_ItemInstanceSave = nullptr;
ItemInstanceLoad_fn Original_ItemInstanceLoad = nullptr;
ItemMineBlock_fn Original_ItemMineBlock = nullptr;
@@ -70,6 +73,7 @@ namespace GameHooks
LevelSetTileAndDataDispatch_fn Original_LevelSetTileAndData = nullptr;
LevelSetDataDispatch_fn Original_LevelSetData = nullptr;
LevelUpdateNeighborsAtDispatch_fn Original_LevelUpdateNeighborsAt = nullptr;
LevelGetData_fn Level_GetData = nullptr;
ServerLevelTickPendingTicks_fn Original_ServerLevelTickPendingTicks = nullptr;
TileGetResource_fn Original_TileGetResource = nullptr;
McRegionChunkStorageLoad_fn Original_McRegionChunkStorageLoad = nullptr;
@@ -241,6 +245,11 @@ namespace GameHooks
static std::atomic<bool> s_tileIdOffsetTried{false};
static int s_tileIdOffset = -1;
static std::atomic<bool> s_tileRendererLevelOffsetTried{false};
static int s_tileRendererLevelOffset = -1;
static std::atomic<bool> s_entityYawOffsetTried{false};
static int s_entityYawOffset = -1;
static int s_rotationLogCount = 0;
static bool ReadFileToString(const char* path, std::string& out)
{
@@ -332,6 +341,71 @@ namespace GameHooks
return true;
}
static bool TryResolveTileRendererLevelOffset()
{
bool expected = false;
if (!s_tileRendererLevelOffsetTried.compare_exchange_strong(expected, true))
return s_tileRendererLevelOffset >= 0;
const char* baseDir = LogUtil::GetBaseDir();
if (!baseDir || baseDir[0] == '\0')
return false;
std::string json;
std::string path = std::string(baseDir) + "metadata\\offsets.json";
if (!ReadFileToString(path.c_str(), json))
{
path = std::string(baseDir) + "offsets.json";
if (!ReadFileToString(path.c_str(), json))
return false;
}
int offset = -1;
if (!ExtractOffsetForField(json, "TileRenderer", "level", offset))
return false;
if (offset <= 0)
return false;
s_tileRendererLevelOffset = offset;
LogUtil::Log("[WeaveLoader] ModelRegistry: TileRenderer.level offset = 0x%X", s_tileRendererLevelOffset);
return true;
}
static bool TryResolveEntityYawOffset()
{
bool expected = false;
if (!s_entityYawOffsetTried.compare_exchange_strong(expected, true))
return s_entityYawOffset >= 0;
const char* baseDir = LogUtil::GetBaseDir();
if (!baseDir || baseDir[0] == '\0')
return false;
std::string json;
std::string path = std::string(baseDir) + "metadata\\offsets.json";
if (!ReadFileToString(path.c_str(), json))
{
path = std::string(baseDir) + "offsets.json";
if (!ReadFileToString(path.c_str(), json))
return false;
}
int offset = -1;
if (!ExtractOffsetForField(json, "Entity", "yRot", offset))
{
if (s_rotationLogCount < 20)
{
LogUtil::Log("[WeaveLoader] Rotation: failed to read Entity.yRot offset");
s_rotationLogCount++;
}
return false;
}
s_entityYawOffset = offset;
LogUtil::Log("[WeaveLoader] ModelRegistry: Entity.yRot offset = 0x%X", s_entityYawOffset);
return true;
}
int GetTileId(void* tilePtr)
{
if (!tilePtr)
@@ -341,6 +415,176 @@ namespace GameHooks
return *reinterpret_cast<int*>(reinterpret_cast<char*>(tilePtr) + s_tileIdOffset);
}
void* GetTileRendererLevel(void* rendererPtr)
{
if (!rendererPtr)
return nullptr;
if (s_tileRendererLevelOffset < 0 && !TryResolveTileRendererLevelOffset())
return nullptr;
if (s_tileRendererLevelOffset <= 0)
return nullptr;
return *reinterpret_cast<void**>(reinterpret_cast<char*>(rendererPtr) + s_tileRendererLevelOffset);
}
static bool TryGetEntityYaw(void* entityPtr, float& outYaw)
{
if (!entityPtr)
return false;
if (s_entityYawOffset < 0 && !TryResolveEntityYawOffset())
return false;
const char* base = reinterpret_cast<const char*>(entityPtr) + s_entityYawOffset;
if (!IsReadableRange(base, sizeof(float)))
return false;
outYaw = *reinterpret_cast<const float*>(base);
return true;
}
static const char* FacingFromDoorDir(int dir)
{
switch (dir & 3)
{
case 0: return "east";
case 1: return "south";
case 2: return "west";
default: return "north";
}
}
static const char* FacingFromFacingDir(int dir)
{
switch (dir & 3)
{
case 0: return "east";
case 1: return "north";
case 2: return "west";
default: return "south";
}
}
static const char* FacingFromTrapdoorDir(int dir)
{
switch (dir & 3)
{
case 0: return "north";
case 1: return "south";
case 2: return "west";
default: return "east";
}
}
static const char* FacingFromWallSignFace(int face)
{
switch (face)
{
case 2: return "north";
case 3: return "south";
case 4: return "west";
case 5: return "east";
default: return "north";
}
}
struct StateProp
{
std::string name;
std::string value;
};
static void AppendProp(std::vector<StateProp>& props, const char* name, const char* value)
{
props.push_back(StateProp{ std::string(name), std::string(value) });
}
static int GetDoorCompositeData(void* levelPtr, int x, int y, int z, int data)
{
if (!levelPtr || !Level_GetData)
return data;
const bool isUpper = (data & 8) != 0;
const int lowerData = isUpper ? Level_GetData(levelPtr, x, y - 1, z) : data;
const int upperData = isUpper ? data : Level_GetData(levelPtr, x, y + 1, z);
const bool rightHinge = (upperData & 1) != 0;
return (lowerData & 7) | (isUpper ? 8 : 0) | (rightHinge ? 16 : 0);
}
static std::string BuildStateKey(int profile, int data, void* levelPtr, int x, int y, int z)
{
std::vector<StateProp> props;
data &= 15;
switch (profile)
{
case 1: // Facing
{
AppendProp(props, "facing", FacingFromFacingDir(data));
break;
}
case 2: // WallSign
{
AppendProp(props, "facing", FacingFromWallSignFace(data));
break;
}
case 3: // StandingSign
{
int rot = data & 15;
std::string rotStr = std::to_string(rot);
AppendProp(props, "rotation", rotStr.c_str());
break;
}
case 4: // Trapdoor
{
AppendProp(props, "facing", FacingFromTrapdoorDir(data));
AppendProp(props, "half", (data & 8) != 0 ? "top" : "bottom");
AppendProp(props, "open", (data & 4) != 0 ? "true" : "false");
break;
}
case 5: // Door
{
const int composite = GetDoorCompositeData(levelPtr, x, y, z, data);
AppendProp(props, "facing", FacingFromDoorDir(composite));
AppendProp(props, "half", (composite & 8) != 0 ? "upper" : "lower");
AppendProp(props, "hinge", (composite & 16) != 0 ? "right" : "left");
AppendProp(props, "open", (composite & 4) != 0 ? "true" : "false");
break;
}
default:
break;
}
if (props.empty())
return std::string();
std::sort(props.begin(), props.end(), [](const StateProp& a, const StateProp& b)
{
return a.name < b.name;
});
std::string key;
for (size_t i = 0; i < props.size(); ++i)
{
if (i > 0)
key.push_back(',');
key.append(props[i].name);
key.push_back('=');
key.append(props[i].value);
}
return key;
}
static bool TryGetModelForState(int blockId, int data, void* levelPtr, int x, int y, int z, const std::vector<ModelBox>*& outBoxes)
{
if (data < 0)
data = 0;
const int profile = ModelRegistry::GetRotationProfile(blockId);
const std::string key = profile != 0 ? BuildStateKey(profile, data, levelPtr, x, y, z) : std::string();
if (!key.empty() && ModelRegistry::TryGetModelVariant(blockId, key.c_str(), outBoxes))
return true;
if (ModelRegistry::TryGetModelVariant(blockId, "", outBoxes))
return true;
return ModelRegistry::TryGetModel(blockId, outBoxes);
}
bool RenderModelInWorld(void* renderer, void* tilePtr, int x, int y, int z, const std::vector<ModelBox>& boxes)
{
if (!renderer || !tilePtr || boxes.empty())
@@ -395,6 +639,9 @@ namespace GameHooks
static int s_itemMineBlockHookCalls = 0;
static void* s_currentLevel = nullptr;
static thread_local void* s_activeUseLevel = nullptr;
static thread_local void* s_lastUsePlayer = nullptr;
static thread_local void* s_lastUseLevel = nullptr;
static thread_local ULONGLONG s_lastUseTimeMs = 0;
static LevelAddEntity_fn s_levelAddEntity = nullptr;
static EntityIONewById_fn s_entityIoNewById = nullptr;
static EntityMoveTo_fn s_entityMoveTo = nullptr;
@@ -1631,6 +1878,30 @@ namespace GameHooks
DispatchManagedBlockUpdate(thisPtr, level, x, y, z, 2, 0);
}
static bool TryGetPlacementPlayer(void* levelPtr, void*& outPlayerPtr)
{
(void)levelPtr;
if (!s_lastUsePlayer)
return false;
const ULONGLONG nowMs = GetTickCount64();
if (nowMs < s_lastUseTimeMs || nowMs - s_lastUseTimeMs > 500)
return false;
outPlayerPtr = s_lastUsePlayer;
return true;
}
static int ComputeFacingDataFromYaw(float yaw)
{
return static_cast<int>(std::floor(((yaw + 180.0f) * 4.0f) / 360.0f - 0.5f)) & 3;
}
static int ComputeStandingSignDataFromYaw(float yaw)
{
return static_cast<int>(std::floor(((yaw + 180.0f) * 16.0f) / 360.0f + 0.5f)) & 15;
}
bool __fastcall Hooked_LevelSetTileAndData(void* thisPtr, int x, int y, int z, int tile, int data, int updateFlags)
{
const int oldBlockId = s_levelGetTile ? s_levelGetTile(thisPtr, x, y, z) : -1;
@@ -1644,6 +1915,51 @@ namespace GameHooks
if (result && tile > 0)
DispatchManagedBlockById(tile, thisPtr, x, y, z, 0, 0);
if (result && tile > 0 && Original_LevelSetData)
{
const int profile = ModelRegistry::GetRotationProfile(tile);
if (profile == 1 || profile == 3)
{
if (s_rotationLogCount < 20)
{
LogUtil::Log("[WeaveLoader] Rotation: tile=%d profile=%d data=%d pos=(%d,%d,%d)", tile, profile, data, x, y, z);
s_rotationLogCount++;
}
void* playerPtr = nullptr;
if (TryGetPlacementPlayer(thisPtr, playerPtr))
{
float yaw = 0.0f;
if (TryGetEntityYaw(playerPtr, yaw))
{
int newData = data;
if (profile == 1)
newData = ComputeFacingDataFromYaw(yaw);
else
newData = ComputeStandingSignDataFromYaw(yaw);
if (newData != data)
Original_LevelSetData(thisPtr, x, y, z, newData, updateFlags, false);
if (s_rotationLogCount < 20)
{
LogUtil::Log("[WeaveLoader] Rotation: yaw=%.2f data=%d -> %d", yaw, data, newData);
s_rotationLogCount++;
}
}
else if (s_rotationLogCount < 20)
{
LogUtil::Log("[WeaveLoader] Rotation: no yaw (offsets loaded=%s)", s_entityYawOffset >= 0 ? "true" : "false");
s_rotationLogCount++;
}
}
else if (s_rotationLogCount < 20)
{
LogUtil::Log("[WeaveLoader] Rotation: no placement player (lastUse=%p)", s_lastUsePlayer);
s_rotationLogCount++;
}
}
}
return result;
}
@@ -2408,6 +2724,24 @@ namespace GameHooks
Original_ItemInstanceMineBlock(thisPtr, level, tile, x, y, z, ownerSharedPtr);
}
bool __fastcall Hooked_ItemInstanceUseOn(void* thisPtr, void* playerSharedPtr, void* level, int x, int y, int z, int face, float clickX, float clickY, float clickZ, bool bTestUseOnOnly)
{
if (!bTestUseOnOnly)
{
void* playerPtr = DecodePlayerPtrFromSharedArg(playerSharedPtr);
if (playerPtr)
{
s_lastUsePlayer = playerPtr;
s_lastUseLevel = level;
s_lastUseTimeMs = GetTickCount64();
}
}
if (Original_ItemInstanceUseOn)
return Original_ItemInstanceUseOn(thisPtr, playerSharedPtr, level, x, y, z, face, clickX, clickY, clickZ, bTestUseOnOnly);
return false;
}
void* __fastcall Hooked_ItemInstanceSave(void* thisPtr, void* compoundTagPtr)
{
// Namespace marker now lives on ItemInstance::tag, so it must be present
@@ -2699,6 +3033,16 @@ namespace GameHooks
s_pendingServerUseItemId = -1;
s_pendingServerUseExpiryMs = 0;
}
if (!effectiveTestUseOnly)
{
void* playerPtr = DecodePlayerPtrFromSharedArg(playerSharedPtr);
if (playerPtr)
{
s_lastUsePlayer = playerPtr;
s_lastUseLevel = level;
s_lastUseTimeMs = nowMs;
}
}
if (s_serverUseLogCount < 40)
{
LogUtil::Log("[WeaveLoader] UseHook ServerPlayerGameMode::useItem test=%d effective=%d item=%d itemPtr=%p playerShared=%p level=%p",
@@ -2726,6 +3070,16 @@ namespace GameHooks
s_pendingServerUseItemId = itemId;
s_pendingServerUseExpiryMs = GetTickCount64() + 1000;
}
if (!bTestUseOnly)
{
void* playerPtr = DecodePlayerPtrFromSharedArg(playerSharedPtr);
if (playerPtr)
{
s_lastUsePlayer = playerPtr;
s_lastUseLevel = level;
s_lastUseTimeMs = GetTickCount64();
}
}
if (s_multiUseLogCount < 40)
{
LogUtil::Log("[WeaveLoader] UseHook MultiPlayerGameMode::useItem test=%d item=%d itemPtr=%p playerShared=%p level=%p",
@@ -3027,10 +3381,24 @@ namespace GameHooks
{
const std::vector<ModelBox>* boxes = nullptr;
int tileId = GetTileId(tilePtr);
if (tileId >= 0 && ModelRegistry::TryGetModel(tileId, boxes) && boxes && !boxes->empty())
if (tileId >= 0)
{
if (RenderModelInWorld(thisPtr, tilePtr, x, y, z, *boxes))
return true;
int data = forceData;
void* levelPtr = nullptr;
if (data < 0 || ModelRegistry::GetRotationProfile(tileId) != 0)
{
levelPtr = GetTileRendererLevel(thisPtr);
if (!levelPtr && s_currentLevel)
levelPtr = s_currentLevel;
if (data < 0 && levelPtr && Level_GetData)
data = Level_GetData(levelPtr, x, y, z);
}
if (TryGetModelForState(tileId, data, levelPtr, x, y, z, boxes) && boxes && !boxes->empty())
{
if (RenderModelInWorld(thisPtr, tilePtr, x, y, z, *boxes))
return true;
}
}
return Original_TileRendererTesselateInWorld
@@ -3042,7 +3410,7 @@ namespace GameHooks
{
const std::vector<ModelBox>* boxes = nullptr;
int tileId = GetTileId(tilePtr);
if (tileId >= 0 && ModelRegistry::TryGetModel(tileId, boxes) && boxes && !boxes->empty())
if (tileId >= 0 && TryGetModelForState(tileId, data, nullptr, 0, 0, 0, boxes) && boxes && !boxes->empty())
{
if (Original_TileRendererRenderTile && Tile_SetShape)
{
@@ -3068,7 +3436,8 @@ namespace GameHooks
{
const std::vector<ModelBox>* boxes = nullptr;
int tileId = GetTileId(thisPtr);
if (tileId >= 0 && ModelRegistry::TryGetModel(tileId, boxes) && boxes && !boxes->empty() && AABB_NewTemp && boxesPtr && boxPtr)
int data = Level_GetData && levelPtr ? Level_GetData(levelPtr, x, y, z) : 0;
if (tileId >= 0 && TryGetModelForState(tileId, data, levelPtr, x, y, z, boxes) && boxes && !boxes->empty() && AABB_NewTemp && boxesPtr && boxPtr)
{
auto list = reinterpret_cast<std::vector<void*>*>(boxesPtr);
const AABBRaw* clipBox = reinterpret_cast<const AABBRaw*>(boxPtr);
@@ -3197,7 +3566,8 @@ namespace GameHooks
const std::vector<ModelBox>* boxes = nullptr;
int tileId = GetTileId(thisPtr);
if (tileId < 0 || !ModelRegistry::TryGetModel(tileId, boxes) || !boxes || boxes->empty())
int data = Level_GetData && levelPtr ? Level_GetData(levelPtr, x, y, z) : 0;
if (tileId < 0 || !TryGetModelForState(tileId, data, levelPtr, x, y, z, boxes) || !boxes || boxes->empty())
{
return Original_TileClip ? Original_TileClip(thisPtr, levelPtr, x, y, z, aPtr, bPtr) : nullptr;
}
@@ -3280,7 +3650,8 @@ namespace GameHooks
continue;
const std::vector<ModelBox>* boxes = nullptr;
if (!ModelRegistry::TryGetModel(tileId, boxes) || !boxes || boxes->empty())
int data = Level_GetData ? Level_GetData(thisPtr, x, y, z) : 0;
if (!TryGetModelForState(tileId, data, thisPtr, x, y, z, boxes) || !boxes || boxes->empty())
continue;
for (const auto& box : *boxes)

View File

@@ -25,6 +25,7 @@ typedef void (__fastcall *ItemRendererRenderItemBillboard_fn)(void* thisPtr, voi
typedef void (__fastcall *AnimatedTextureCycleFrames_fn)(void* thisPtr);
typedef int (__fastcall *TextureGetSourceDim_fn)(void* thisPtr);
typedef void (__fastcall *ItemInstanceMineBlock_fn)(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
typedef bool (__fastcall *ItemInstanceUseOn_fn)(void* thisPtr, void* playerSharedPtr, void* level, int x, int y, int z, int face, float clickX, float clickY, float clickZ, bool bTestUseOnOnly);
typedef void* (__fastcall *ItemInstanceSave_fn)(void* thisPtr, void* compoundTagPtr);
typedef void (__fastcall *ItemInstanceLoad_fn)(void* thisPtr, void* compoundTagPtr);
typedef bool (__fastcall *ItemMineBlock_fn)(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
@@ -38,6 +39,7 @@ typedef bool (__fastcall *LevelSetDataDispatch_fn)(void* thisPtr, int x, int y,
typedef void (__fastcall *LevelUpdateNeighborsAtDispatch_fn)(void* thisPtr, int x, int y, int z, int type);
typedef bool (__fastcall *ServerLevelTickPendingTicks_fn)(void* thisPtr, bool force);
typedef int (__fastcall *LevelGetTile_fn)(void* thisPtr, int x, int y, int z);
typedef int (__fastcall *LevelGetData_fn)(void* thisPtr, int x, int y, int z);
typedef void* (__fastcall *McRegionChunkStorageLoad_fn)(void* thisPtr, void* level, int x, int z);
typedef void (__fastcall *McRegionChunkStorageSave_fn)(void* thisPtr, void* level, void* levelChunk);
typedef int (__fastcall *TileGetResource_fn)(void* thisPtr, int data, void* random, int playerBonusLevel);
@@ -115,6 +117,7 @@ namespace GameHooks
extern TextureGetSourceDim_fn Original_ClockTextureGetSourceWidth;
extern TextureGetSourceDim_fn Original_ClockTextureGetSourceHeight;
extern ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock;
extern ItemInstanceUseOn_fn Original_ItemInstanceUseOn;
extern ItemInstanceSave_fn Original_ItemInstanceSave;
extern ItemInstanceLoad_fn Original_ItemInstanceLoad;
extern ItemMineBlock_fn Original_ItemMineBlock;
@@ -129,6 +132,7 @@ namespace GameHooks
extern LevelSetTileAndDataDispatch_fn Original_LevelSetTileAndData;
extern LevelSetDataDispatch_fn Original_LevelSetData;
extern LevelUpdateNeighborsAtDispatch_fn Original_LevelUpdateNeighborsAt;
extern LevelGetData_fn Level_GetData;
extern ServerLevelTickPendingTicks_fn Original_ServerLevelTickPendingTicks;
extern TileGetResource_fn Original_TileGetResource;
extern McRegionChunkStorageLoad_fn Original_McRegionChunkStorageLoad;
@@ -206,6 +210,7 @@ namespace GameHooks
int __fastcall Hooked_ClockTextureGetSourceWidth(void* thisPtr);
int __fastcall Hooked_ClockTextureGetSourceHeight(void* thisPtr);
void __fastcall Hooked_ItemInstanceMineBlock(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
bool __fastcall Hooked_ItemInstanceUseOn(void* thisPtr, void* playerSharedPtr, void* level, int x, int y, int z, int face, float clickX, float clickY, float clickZ, bool bTestUseOnOnly);
void* __fastcall Hooked_ItemInstanceSave(void* thisPtr, void* compoundTagPtr);
void __fastcall Hooked_ItemInstanceLoad(void* thisPtr, void* compoundTagPtr);
bool __fastcall Hooked_ItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);

View File

@@ -45,6 +45,8 @@ bool HookManager::Install(const SymbolResolver& symbols)
reinterpret_cast<Vec3NewTemp_fn>(symbols.Tile.pVec3NewTemp);
GameHooks::HitResult_Ctor =
reinterpret_cast<HitResultCtor_fn>(symbols.Tile.pHitResultCtor);
GameHooks::Level_GetData =
reinterpret_cast<LevelGetData_fn>(symbols.Level.pLevelGetData);
if (symbols.Core.pRunStaticCtors)
{
@@ -110,6 +112,20 @@ bool HookManager::Install(const SymbolResolver& symbols)
}
}
if (symbols.Item.pItemInstanceUseOn)
{
if (MH_CreateHook(symbols.Item.pItemInstanceUseOn,
reinterpret_cast<void*>(&GameHooks::Hooked_ItemInstanceUseOn),
reinterpret_cast<void**>(&GameHooks::Original_ItemInstanceUseOn)) != MH_OK)
{
LogUtil::Log("[WeaveLoader] Warning: Failed to hook ItemInstance::useOn");
}
else
{
LogUtil::Log("[WeaveLoader] Hooked ItemInstance::useOn (placement tracking)");
}
}
if (symbols.Item.pItemInstanceSave)
{
if (MH_CreateHook(symbols.Item.pItemInstanceSave,

View File

@@ -2,10 +2,18 @@
#include "LogUtil.h"
#include <unordered_map>
#include <mutex>
#include <string>
namespace
{
std::unordered_map<int, std::vector<ModelBox>> g_models;
struct BlockModelEntry
{
std::vector<ModelBox> base;
std::unordered_map<std::string, std::vector<ModelBox>> variants;
int rotationProfile = 0;
};
std::unordered_map<int, BlockModelEntry> g_models;
std::mutex g_mutex;
}
@@ -21,18 +29,70 @@ void ModelRegistry::RegisterBlockModel(int blockId, const ModelBox* boxes, int c
{
std::lock_guard<std::mutex> guard(g_mutex);
g_models[blockId] = std::move(data);
g_models[blockId].base = std::move(data);
}
LogUtil::Log("[WeaveLoader] ModelRegistry: registered %d box(es) for block %d", count, blockId);
}
void ModelRegistry::RegisterBlockModelVariant(int blockId, const char* key, const ModelBox* boxes, int count)
{
if (blockId < 0 || !key || !boxes || count <= 0)
return;
std::vector<ModelBox> data;
data.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; i++)
data.push_back(boxes[i]);
{
std::lock_guard<std::mutex> guard(g_mutex);
g_models[blockId].variants[std::string(key)] = std::move(data);
}
LogUtil::Log("[WeaveLoader] ModelRegistry: registered variant '%s' (%d box(es)) for block %d", key, count, blockId);
}
void ModelRegistry::SetRotationProfile(int blockId, int profile)
{
if (blockId < 0)
return;
std::lock_guard<std::mutex> guard(g_mutex);
g_models[blockId].rotationProfile = profile;
}
int ModelRegistry::GetRotationProfile(int blockId)
{
std::lock_guard<std::mutex> guard(g_mutex);
auto it = g_models.find(blockId);
if (it == g_models.end())
return 0;
return it->second.rotationProfile;
}
bool ModelRegistry::TryGetModel(int blockId, const std::vector<ModelBox>*& outBoxes)
{
std::lock_guard<std::mutex> guard(g_mutex);
auto it = g_models.find(blockId);
if (it == g_models.end())
return false;
outBoxes = &it->second;
if (it->second.base.empty())
return false;
outBoxes = &it->second.base;
return true;
}
bool ModelRegistry::TryGetModelVariant(int blockId, const char* key, const std::vector<ModelBox>*& outBoxes)
{
if (!key)
return false;
std::lock_guard<std::mutex> guard(g_mutex);
auto it = g_models.find(blockId);
if (it == g_models.end())
return false;
auto vit = it->second.variants.find(std::string(key));
if (vit == it->second.variants.end())
return false;
outBoxes = &vit->second;
return true;
}

View File

@@ -15,5 +15,9 @@ struct ModelBox
namespace ModelRegistry
{
void RegisterBlockModel(int blockId, const ModelBox* boxes, int count);
void RegisterBlockModelVariant(int blockId, const char* key, const ModelBox* boxes, int count);
void SetRotationProfile(int blockId, int profile);
int GetRotationProfile(int blockId);
bool TryGetModel(int blockId, const std::vector<ModelBox>*& outBoxes);
bool TryGetModelVariant(int blockId, const char* key, const std::vector<ModelBox>*& outBoxes);
}

View File

@@ -312,6 +312,16 @@ void native_register_block_model(int blockId, const ModelBox* boxes, int count)
ModelRegistry::RegisterBlockModel(blockId, boxes, count);
}
void native_register_block_model_variant(int blockId, const char* key, const ModelBox* boxes, int count)
{
ModelRegistry::RegisterBlockModelVariant(blockId, key, boxes, count);
}
void native_register_block_rotation_profile(int blockId, int profile)
{
ModelRegistry::SetRotationProfile(blockId, profile);
}
void native_configure_managed_block(int numericBlockId, int dropNumericBlockId, int cloneNumericBlockId)
{
if (numericBlockId < 0)

View File

@@ -96,6 +96,14 @@ extern "C"
int blockId,
const ModelBox* boxes,
int count);
__declspec(dllexport) void native_register_block_model_variant(
int blockId,
const char* key,
const ModelBox* boxes,
int count);
__declspec(dllexport) void native_register_block_rotation_profile(
int blockId,
int profile);
__declspec(dllexport) int native_register_item(
const char* namespacedId,

View File

@@ -63,6 +63,7 @@ namespace
static const char* SYM_ITEMINSTANCE_GETICON = "?getIcon@ItemInstance@@QEAAPEAVIcon@@XZ";
static const char* SYM_ITEMINSTANCE_MINEBLOCK = "?mineBlock@ItemInstance@@QEAAXPEAVLevel@@HHHHV?$shared_ptr@VPlayer@@@std@@@Z";
static const char* SYM_ITEMINSTANCE_USEON = "?useOn@ItemInstance@@QEAA_NV?$shared_ptr@VPlayer@@@std@@PEAVLevel@@HHHHMMM_N@Z";
static const char* SYM_ITEMINSTANCE_SAVE = "?save@ItemInstance@@QEAAPEAVCompoundTag@@PEAV2@@Z";
static const char* SYM_ITEMINSTANCE_LOAD = "?load@ItemInstance@@QEAAXPEAVCompoundTag@@@Z";
static const char* SYM_ITEMINSTANCE_HURTANDBREAK = "?hurtAndBreak@ItemInstance@@QEAAXHV?$shared_ptr@VLivingEntity@@@std@@@Z";
@@ -112,6 +113,7 @@ namespace
static const char* SYM_LEVEL_UPDATE_NEIGHBORS_AT = "?updateNeighborsAt@Level@@QEAAXHHHH@Z";
static const char* SYM_SERVERLEVEL_TICKPENDINGTICKS = "?tickPendingTicks@ServerLevel@@UEAA_N_N@Z";
static const char* SYM_LEVEL_GETTILE = "?getTile@Level@@UEAAHHHH@Z";
static const char* SYM_LEVEL_GETDATA = "?getData@Level@@UEAAHHHH@Z";
static const char* SYM_LEVEL_SETDATA = "?setData@Level@@UEAA_NHHHHH_N@Z";
static const char* SYM_LEVEL_CLIP = "?clip@Level@@QEAAPEAVHitResult@@PEAVVec3@@0_N1@Z";
static const char* SYM_MCREGIONCHUNKSTORAGE_LOAD = "?load@McRegionChunkStorage@@UEAAPEAVLevelChunk@@PEAVLevel@@HH@Z";
@@ -296,6 +298,7 @@ bool ItemSymbols::Resolve(SymbolResolver& resolver)
{
pItemInstanceGetIcon = resolver.Resolve(SYM_ITEMINSTANCE_GETICON);
pItemInstanceMineBlock = resolver.Resolve(SYM_ITEMINSTANCE_MINEBLOCK);
pItemInstanceUseOn = resolver.Resolve(SYM_ITEMINSTANCE_USEON);
pItemInstanceSave = resolver.Resolve(SYM_ITEMINSTANCE_SAVE);
pItemInstanceLoad = resolver.Resolve(SYM_ITEMINSTANCE_LOAD);
pItemInstanceHurtAndBreak = resolver.Resolve(SYM_ITEMINSTANCE_HURTANDBREAK);
@@ -314,6 +317,7 @@ void ItemSymbols::Log() const
{
LogSym("ItemInstance::getIcon", pItemInstanceGetIcon);
LogSym("ItemInstance::mineBlock", pItemInstanceMineBlock);
LogSym("ItemInstance::useOn", pItemInstanceUseOn);
LogSym("ItemInstance::save", pItemInstanceSave);
LogSym("ItemInstance::load", pItemInstanceLoad);
LogSym("ItemInstance::hurtAndBreak", pItemInstanceHurtAndBreak);
@@ -420,6 +424,7 @@ bool LevelSymbols::Resolve(SymbolResolver& resolver)
pLevelUpdateNeighborsAt = resolver.Resolve(SYM_LEVEL_UPDATE_NEIGHBORS_AT);
pServerLevelTickPendingTicks = resolver.Resolve(SYM_SERVERLEVEL_TICKPENDINGTICKS);
pLevelGetTile = resolver.Resolve(SYM_LEVEL_GETTILE);
pLevelGetData = resolver.Resolve(SYM_LEVEL_GETDATA);
pLevelSetData = resolver.Resolve(SYM_LEVEL_SETDATA);
pLevelClip = resolver.Resolve(SYM_LEVEL_CLIP);
pMcRegionChunkStorageLoad = resolver.Resolve(SYM_MCREGIONCHUNKSTORAGE_LOAD);
@@ -445,6 +450,7 @@ void LevelSymbols::Log() const
LogSym("Level::updateNeighborsAt", pLevelUpdateNeighborsAt);
LogSym("ServerLevel::tickPendingTicks", pServerLevelTickPendingTicks);
LogSym("Level::getTile", pLevelGetTile);
LogSym("Level::getData", pLevelGetData);
LogSym("Level::setData", pLevelSetData);
LogSym("Level::clip", pLevelClip);
LogSym("McRegionChunkStorage::load", pMcRegionChunkStorageLoad);

View File

@@ -77,6 +77,7 @@ struct ItemSymbols
{
void* pItemInstanceGetIcon = nullptr;
void* pItemInstanceMineBlock = nullptr;
void* pItemInstanceUseOn = nullptr;
void* pItemInstanceSave = nullptr;
void* pItemInstanceLoad = nullptr;
void* pItemInstanceHurtAndBreak = nullptr;
@@ -138,6 +139,7 @@ struct LevelSymbols
void* pLevelUpdateNeighborsAt = nullptr;
void* pServerLevelTickPendingTicks = nullptr;
void* pLevelGetTile = nullptr;
void* pLevelGetData = nullptr;
void* pLevelSetData = nullptr;
void* pLevelClip = nullptr;
void* pMcRegionChunkStorageLoad = nullptr;