mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-06-11 15:31:55 +00:00
feat(blockstates): rotation profiles and placement tracking
This commit is contained in:
@@ -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));
|
||||
|
||||
|
||||
19
ExampleMod/assets/examplemod/blockstates/ruby_chair.json
Normal file
19
ExampleMod/assets/examplemod/blockstates/ruby_chair.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user