Compare commits

...

8 Commits

11 changed files with 396 additions and 333 deletions

View File

@@ -1,5 +1,5 @@
{
"total": 73071,
"total": 75390,
"sessions": [
{
"begin": "2026-03-11T23:50:47+01:00",
@@ -70,6 +70,11 @@
"begin": "2026-03-15T21:00:39+01:00",
"end": "2026-03-15T23:04:35+01:00",
"duration": 7435
},
{
"begin": "2026-03-28T03:48:55+01:00",
"end": "2026-03-28T04:27:34+01:00",
"duration": 2319
}
]
}

View File

@@ -6,6 +6,8 @@ public sealed class Config
public bool HoldKey { get; set; } = true;
public bool BillboardIgnoreFront { get; set; } = false;
public int BillboardColumnsPerBlock { get; set; } = 4;
public int ColumnsUnderCursor { get; set; } = 10;

View File

@@ -117,8 +117,20 @@ internal class CardRenderer(ICoreClientAPI api, Config config) : IDisposable
if (heldBag == null)
continue;
ItemStack[]? bagContents;
try
{
bagContents = heldBag.GetContents(bagStack, api.World);
}
catch
{
continue;
}
if (bagContents == null)
continue;
contents = [];
ItemStack[] bagContents = heldBag.GetContents(bagStack, api.World);
foreach (ItemStack contentStack in bagContents)
{
if (contentStack == null || contentStack.StackSize <= 0)

View File

@@ -1,329 +1,330 @@
using ChestPreview.Configs;
using ChestPreview.Models;
using ChestPreview.Utils;
using System;
using System.Collections.Generic;
using System.Numerics;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.MathTools;
using Vintagestory.GameContent;
namespace ChestPreview.Rendering;
internal class WorldBillboardPresenter(ICoreClientAPI api)
{
private const int multiblock_scan_radius = 2;
private static readonly float front_face_offset = 0.01f;
private readonly ICoreClientAPI api = api;
private readonly List<BillboardTarget> frameBillboards = [];
public IReadOnlyList<BillboardTarget> FrameBillboards => frameBillboards;
public void PrepareFrame(List<PreviewTarget> previewTargets)
{
frameBillboards.Clear();
foreach (PreviewTarget previewTarget in previewTargets)
{
if (!CanPresentAsBillboard(previewTarget))
continue;
if (!TryCreateBillboardTarget(previewTarget, out BillboardTarget billboardTarget))
continue;
frameBillboards.Add(billboardTarget);
}
}
public void Clear()
{
frameBillboards.Clear();
}
private static bool CanPresentAsBillboard(PreviewTarget previewTarget)
{
return previewTarget.Mode is PreviewModes.ON_HOVERED_CONTAINER or PreviewModes.ON_NEARBY_CONTAINERS;
}
private bool TryCreateBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget)
{
return TryCreateFrontBillboardTarget(previewTarget, out billboardTarget);
}
private bool TryCreateFrontBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget)
{
billboardTarget = default;
Block block = previewTarget.Block;
BlockPos blockPos = previewTarget.BlockEntity.Pos;
GetContainerBounds(block, blockPos, out Vector3 min, out Vector3 max);
if (!TryResolveFacingNormal(previewTarget.Block, previewTarget.BlockEntity, out Vec3f forward, out Vec3f right, out Vec3f up))
return false;
Vector3 centerLocal = (min + max) * 0.5f;
Vector3 halfExtents = (max - min) * 0.5f;
Vector3 rightVector = right.ToVector3();
Vector3 upVector = up.ToVector3();
Vector3 forwardVector = forward.ToVector3();
float halfWidth = Vector3.Dot(Vector3.Abs(rightVector), halfExtents);
float halfHeight = Vector3.Dot(Vector3.Abs(upVector), halfExtents);
float halfDepth = ResolveStableHalfDepth(forwardVector, halfExtents);
float width = halfWidth * 2f;
float height = halfHeight * 2f;
Vector3 center = blockPos.ToVector3() + centerLocal + forwardVector * (halfDepth + front_face_offset);
width = Math.Max(0.05f, width);
height = Math.Max(0.05f, height);
billboardTarget = new BillboardTarget(previewTarget, center.ToVec3d(), right, up, forward, width, height, true);
return true;
}
private static float ResolveStableHalfDepth(Vector3 forward, Vector3 halfExtents)
{
if (MathF.Abs(forward.Y) < 0.5f)
return MathF.Abs(forward.X) >= MathF.Abs(forward.Z) ? halfExtents.X : halfExtents.Z;
return MathF.Abs(forward.Y) >= MathF.Abs(forward.Z) ? halfExtents.Y : halfExtents.Z;
}
private void GetContainerBounds(Block block, BlockPos blockPos, out Vector3 min, out Vector3 max)
{
min = new Vector3(0f, 0f, 0f);
max = new Vector3(1f, 1f, 1f);
ExpandBoundsFromSelectionBoxes(block, blockPos, blockPos, ref min, ref max);
ExpandBoundsFromLinkedMultiblockParts(blockPos, ref min, ref max);
}
private void ExpandBoundsFromLinkedMultiblockParts(BlockPos controllerPos, ref Vector3 min, ref Vector3 max)
{
int minScanX = controllerPos.X - multiblock_scan_radius;
int maxScanX = controllerPos.X + multiblock_scan_radius;
int minScanY = controllerPos.Y - multiblock_scan_radius;
int maxScanY = controllerPos.Y + multiblock_scan_radius;
int minScanZ = controllerPos.Z - multiblock_scan_radius;
int maxScanZ = controllerPos.Z + multiblock_scan_radius;
Vector3 minLocal = min;
Vector3 maxLocal = max;
api.World.BlockAccessor.WalkBlocks(new BlockPos(minScanX, minScanY, minScanZ), new BlockPos(maxScanX, maxScanY, maxScanZ), (partBlock, x, y, z) =>
{
if (x == controllerPos.X && y == controllerPos.Y && z == controllerPos.Z)
return;
if (partBlock.Id == 0 || partBlock is not IMultiblockOffset multiblockOffset)
return;
BlockPos partPos = new(x, y, z);
BlockPos linkedControllerPos = multiblockOffset.GetControlBlockPos(partPos);
if (!IsSameBlockPos(linkedControllerPos, controllerPos))
return;
ExpandBoundsFromSelectionBoxes(partBlock, partPos, controllerPos, ref minLocal, ref maxLocal);
});
min = minLocal;
max = maxLocal;
}
private void ExpandBoundsFromSelectionBoxes(Block block, BlockPos sourcePos, BlockPos originPos, ref Vector3 min, ref Vector3 max)
{
Cuboidf[]? selectionBoxes = block.GetSelectionBoxes(api.World.BlockAccessor, sourcePos);
Vector3 sourceOffset = sourcePos.ToVector3() - originPos.ToVector3();
if (selectionBoxes is not { Length: > 0 })
{
min = Vector3.Min(min, sourceOffset);
max = Vector3.Max(max, sourceOffset + Vector3.One);
return;
}
foreach (Cuboidf selectionBox in selectionBoxes)
{
min = Vector3.Min(min, sourceOffset + selectionBox.Start.ToVector3());
max = Vector3.Max(max, sourceOffset + selectionBox.End.ToVector3());
}
}
private static bool IsSameBlockPos(BlockPos a, BlockPos b)
{
return a.X == b.X && a.Y == b.Y && a.Z == b.Z;
}
private bool TryResolveFacingNormal(Block block, BlockEntity blockEntity, out Vec3f forward, out Vec3f right, out Vec3f up)
{
forward = new Vec3f(0f, 0f, 1f);
right = new Vec3f(1f, 0f, 0f);
up = new Vec3f(0f, 1f, 0f);
if (TryGetMeshAngle(blockEntity, out float meshAngleRadians))
{
// MeshAngle is around Y axis in radians.
forward = new Vec3f(MathF.Sin(meshAngleRadians), 0f, MathF.Cos(meshAngleRadians));
NormalizeFacing(ref forward);
BuildBasisFromForward(forward, out right, out up);
return true;
}
if (TryGetBlockSideFacing(block, out forward))
{
BuildBasisFromForward(forward, out right, out up);
return true;
}
if (!TryGetPlayerFacing(blockEntity.Pos, out forward))
return false;
BuildBasisFromForward(forward, out right, out up);
return true;
}
private bool TryGetPlayerFacing(BlockPos blockPos, out Vec3f facing)
{
facing = new Vec3f(0f, 0f, 1f);
EntityPlayer? playerEntity = api.World.Player?.Entity;
if (playerEntity?.CameraPos == null)
return false;
Vec3d cameraPos = playerEntity.CameraPos + playerEntity.LocalEyePos;
double dx = cameraPos.X - (blockPos.X + 0.5d);
double dz = cameraPos.Z - (blockPos.Z + 0.5d);
if (Math.Abs(dx) <= 0.0001d && Math.Abs(dz) <= 0.0001d)
return false;
double absDx = Math.Abs(dx);
double absDz = Math.Abs(dz);
double diagonalTolerance = Math.Max(absDx, absDz) * 0.65d;
bool isDiagonalView = Math.Abs(absDx - absDz) <= diagonalTolerance;
if (isDiagonalView)
{
Vec3f facingX = dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f);
Vec3f facingZ = dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f);
int obstructionX = GetFacingObstructionScore(blockPos, facingX);
int obstructionZ = GetFacingObstructionScore(blockPos, facingZ);
if (obstructionX != obstructionZ)
{
facing = obstructionX < obstructionZ ? facingX : facingZ;
return true;
}
}
facing = absDx >= absDz
? (dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f))
: (dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f));
return true;
}
private int GetFacingObstructionScore(BlockPos blockPos, Vec3f facing)
{
int offsetX = Math.Sign(facing.X);
int offsetZ = Math.Sign(facing.Z);
int score = 0;
for (int yOffset = 0; yOffset <= 1; yOffset++)
{
BlockPos checkPos = new(blockPos.X + offsetX, blockPos.Y + yOffset, blockPos.Z + offsetZ);
if (IsSolidBlock(checkPos))
score++;
}
return score;
}
private bool IsSolidBlock(BlockPos blockPos)
{
Block block = api.World.BlockAccessor.GetBlock(blockPos);
if (block.Id == 0)
return false;
Cuboidf[]? collisionBoxes = block.GetCollisionBoxes(api.World.BlockAccessor, blockPos);
return collisionBoxes is { Length: > 0 };
}
private static bool TryGetBlockSideFacing(Block block, out Vec3f facing)
{
facing = new Vec3f(0f, 0f, 1f);
string? side = block.Variant?["side"];
if (string.IsNullOrWhiteSpace(side))
return false;
switch (side.ToLowerInvariant())
{
case "north":
facing = new Vec3f(0f, 0f, -1f);
return true;
case "south":
facing = new Vec3f(0f, 0f, 1f);
return true;
case "east":
facing = new Vec3f(1f, 0f, 0f);
return true;
case "west":
facing = new Vec3f(-1f, 0f, 0f);
return true;
case "up":
facing = new Vec3f(0f, 1f, 0f);
return true;
case "down":
facing = new Vec3f(0f, -1f, 0f);
return true;
default:
return false;
}
}
private static void BuildBasisFromForward(Vec3f forward, out Vec3f right, out Vec3f up)
{
up = new Vec3f(0f, 1f, 0f);
if (Math.Abs(forward.Y) > 0.5f)
{
up = forward.Y > 0f ? new Vec3f(0f, 0f, -1f) : new Vec3f(0f, 0f, 1f);
right = new Vec3f(1f, 0f, 0f);
return;
}
right = new Vec3f(-forward.Z, 0f, forward.X);
}
private static void NormalizeFacing(ref Vec3f facing)
{
float length = MathF.Sqrt(facing.X * facing.X + facing.Y * facing.Y + facing.Z * facing.Z);
if (length <= 0.0001f)
{
facing = new Vec3f(0f, 0f, 1f);
return;
}
float invLength = 1f / length;
facing = new Vec3f(facing.X * invLength, facing.Y * invLength, facing.Z * invLength);
}
private static bool TryGetMeshAngle(BlockEntity blockEntity, out float radians)
{
if (blockEntity is BlockEntityGenericTypedContainer genericContainer)
{
radians = genericContainer.MeshAngle;
return true;
}
if (blockEntity is BlockEntityCrate crate)
{
radians = crate.MeshAngle;
return true;
}
radians = 0f;
return false;
}
}
using ChestPreview.Configs;
using ChestPreview.Models;
using ChestPreview.Utils;
using System;
using System.Collections.Generic;
using System.Numerics;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.MathTools;
using Vintagestory.GameContent;
namespace ChestPreview.Rendering;
internal class WorldBillboardPresenter(ICoreClientAPI api, Config config)
{
private const int multiblock_scan_radius = 2;
private static readonly float front_face_offset = 0.05f;
private readonly ICoreClientAPI api = api;
private readonly Config config = config;
private readonly List<BillboardTarget> frameBillboards = [];
public IReadOnlyList<BillboardTarget> FrameBillboards => frameBillboards;
public void PrepareFrame(List<PreviewTarget> previewTargets)
{
frameBillboards.Clear();
foreach (PreviewTarget previewTarget in previewTargets)
{
if (!CanPresentAsBillboard(previewTarget))
continue;
if (!TryCreateBillboardTarget(previewTarget, out BillboardTarget billboardTarget))
continue;
frameBillboards.Add(billboardTarget);
}
}
public void Clear()
{
frameBillboards.Clear();
}
private static bool CanPresentAsBillboard(PreviewTarget previewTarget)
{
return previewTarget.Mode is PreviewModes.ON_HOVERED_CONTAINER or PreviewModes.ON_NEARBY_CONTAINERS;
}
private bool TryCreateBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget)
{
return TryCreateFrontBillboardTarget(previewTarget, out billboardTarget);
}
private bool TryCreateFrontBillboardTarget(PreviewTarget previewTarget, out BillboardTarget billboardTarget)
{
billboardTarget = default;
Block block = previewTarget.Block;
BlockPos blockPos = previewTarget.BlockEntity.Pos;
GetContainerBounds(block, blockPos, out Vector3 min, out Vector3 max);
if (!TryResolveFacingNormal(previewTarget.Block, previewTarget.BlockEntity, out Vec3f forward, out Vec3f right, out Vec3f up))
return false;
Vector3 centerLocal = (min + max) * 0.5f;
Vector3 halfExtents = (max - min) * 0.5f;
Vector3 rightVector = right.ToVector3();
Vector3 upVector = up.ToVector3();
Vector3 forwardVector = forward.ToVector3();
float halfWidth = Vector3.Dot(Vector3.Abs(rightVector), halfExtents);
float halfHeight = Vector3.Dot(Vector3.Abs(upVector), halfExtents);
float halfDepth = ResolveStableHalfDepth(forwardVector, halfExtents);
float width = halfWidth * 2f;
float height = halfHeight * 2f;
Vector3 center = blockPos.ToVector3() + centerLocal + forwardVector * (halfDepth + front_face_offset);
width = Math.Max(0.05f, width);
height = Math.Max(0.05f, height);
billboardTarget = new BillboardTarget(previewTarget, center.ToVec3d(), right, up, forward, width, height, true);
return true;
}
private static float ResolveStableHalfDepth(Vector3 forward, Vector3 halfExtents)
{
if (MathF.Abs(forward.Y) < 0.5f)
return MathF.Abs(forward.X) >= MathF.Abs(forward.Z) ? halfExtents.X : halfExtents.Z;
return MathF.Abs(forward.Y) >= MathF.Abs(forward.Z) ? halfExtents.Y : halfExtents.Z;
}
private void GetContainerBounds(Block block, BlockPos blockPos, out Vector3 min, out Vector3 max)
{
min = new Vector3(0f, 0f, 0f);
max = new Vector3(1f, 1f, 1f);
ExpandBoundsFromSelectionBoxes(block, blockPos, blockPos, ref min, ref max);
ExpandBoundsFromLinkedMultiblockParts(blockPos, ref min, ref max);
}
private void ExpandBoundsFromLinkedMultiblockParts(BlockPos controllerPos, ref Vector3 min, ref Vector3 max)
{
int minScanX = controllerPos.X - multiblock_scan_radius;
int maxScanX = controllerPos.X + multiblock_scan_radius;
int minScanY = controllerPos.Y - multiblock_scan_radius;
int maxScanY = controllerPos.Y + multiblock_scan_radius;
int minScanZ = controllerPos.Z - multiblock_scan_radius;
int maxScanZ = controllerPos.Z + multiblock_scan_radius;
Vector3 minLocal = min;
Vector3 maxLocal = max;
api.World.BlockAccessor.WalkBlocks(new BlockPos(minScanX, minScanY, minScanZ), new BlockPos(maxScanX, maxScanY, maxScanZ), (partBlock, x, y, z) =>
{
if (x == controllerPos.X && y == controllerPos.Y && z == controllerPos.Z)
return;
if (partBlock.Id == 0 || partBlock is not IMultiblockOffset multiblockOffset)
return;
BlockPos partPos = new(x, y, z);
BlockPos linkedControllerPos = multiblockOffset.GetControlBlockPos(partPos);
if (!IsSameBlockPos(linkedControllerPos, controllerPos))
return;
ExpandBoundsFromSelectionBoxes(partBlock, partPos, controllerPos, ref minLocal, ref maxLocal);
});
min = minLocal;
max = maxLocal;
}
private void ExpandBoundsFromSelectionBoxes(Block block, BlockPos sourcePos, BlockPos originPos, ref Vector3 min, ref Vector3 max)
{
Cuboidf[]? selectionBoxes = block.GetSelectionBoxes(api.World.BlockAccessor, sourcePos);
Vector3 sourceOffset = sourcePos.ToVector3() - originPos.ToVector3();
if (selectionBoxes is not { Length: > 0 })
{
min = Vector3.Min(min, sourceOffset);
max = Vector3.Max(max, sourceOffset + Vector3.One);
return;
}
foreach (Cuboidf selectionBox in selectionBoxes)
{
min = Vector3.Min(min, sourceOffset + selectionBox.Start.ToVector3());
max = Vector3.Max(max, sourceOffset + selectionBox.End.ToVector3());
}
}
private static bool IsSameBlockPos(BlockPos a, BlockPos b)
{
return a.X == b.X && a.Y == b.Y && a.Z == b.Z;
}
private bool TryResolveFacingNormal(Block block, BlockEntity blockEntity, out Vec3f forward, out Vec3f right, out Vec3f up)
{
forward = new Vec3f(0f, 0f, 1f);
right = new Vec3f(1f, 0f, 0f);
up = new Vec3f(0f, 1f, 0f);
if (!config.BillboardIgnoreFront && TryGetMeshAngle(blockEntity, out float meshAngleRadians))
{
// MeshAngle is around Y axis in radians.
forward = new Vec3f(MathF.Sin(meshAngleRadians), 0f, MathF.Cos(meshAngleRadians));
NormalizeFacing(ref forward);
BuildBasisFromForward(forward, out right, out up);
return true;
}
if (!config.BillboardIgnoreFront && TryGetBlockSideFacing(block, out forward))
{
BuildBasisFromForward(forward, out right, out up);
return true;
}
if (!TryGetPlayerFacing(blockEntity.Pos, out forward))
return false;
BuildBasisFromForward(forward, out right, out up);
return true;
}
private bool TryGetPlayerFacing(BlockPos blockPos, out Vec3f facing)
{
facing = new Vec3f(0f, 0f, 1f);
EntityPlayer? playerEntity = api.World.Player?.Entity;
if (playerEntity?.CameraPos == null)
return false;
Vec3d cameraPos = playerEntity.CameraPos + playerEntity.LocalEyePos;
double dx = cameraPos.X - (blockPos.X + 0.5d);
double dz = cameraPos.Z - (blockPos.Z + 0.5d);
if (Math.Abs(dx) <= 0.0001d && Math.Abs(dz) <= 0.0001d)
return false;
double absDx = Math.Abs(dx);
double absDz = Math.Abs(dz);
double diagonalTolerance = Math.Max(absDx, absDz) * 0.65d;
bool isDiagonalView = Math.Abs(absDx - absDz) <= diagonalTolerance;
if (isDiagonalView)
{
Vec3f facingX = dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f);
Vec3f facingZ = dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f);
int obstructionX = GetFacingObstructionScore(blockPos, facingX);
int obstructionZ = GetFacingObstructionScore(blockPos, facingZ);
if (obstructionX != obstructionZ)
{
facing = obstructionX < obstructionZ ? facingX : facingZ;
return true;
}
}
facing = absDx >= absDz
? (dx >= 0d ? new Vec3f(1f, 0f, 0f) : new Vec3f(-1f, 0f, 0f))
: (dz >= 0d ? new Vec3f(0f, 0f, 1f) : new Vec3f(0f, 0f, -1f));
return true;
}
private int GetFacingObstructionScore(BlockPos blockPos, Vec3f facing)
{
int offsetX = Math.Sign(facing.X);
int offsetZ = Math.Sign(facing.Z);
int score = 0;
for (int yOffset = 0; yOffset <= 1; yOffset++)
{
BlockPos checkPos = new(blockPos.X + offsetX, blockPos.Y + yOffset, blockPos.Z + offsetZ);
if (IsSolidBlock(checkPos))
score++;
}
return score;
}
private bool IsSolidBlock(BlockPos blockPos)
{
Block block = api.World.BlockAccessor.GetBlock(blockPos);
if (block.Id == 0)
return false;
Cuboidf[]? collisionBoxes = block.GetCollisionBoxes(api.World.BlockAccessor, blockPos);
return collisionBoxes is { Length: > 0 };
}
private static bool TryGetBlockSideFacing(Block block, out Vec3f facing)
{
facing = new Vec3f(0f, 0f, 1f);
string? side = block.Variant?["side"];
if (string.IsNullOrWhiteSpace(side))
return false;
switch (side.ToLowerInvariant())
{
case "north":
facing = new Vec3f(0f, 0f, 1f);
return true;
case "south":
facing = new Vec3f(0f, 0f, -1f);
return true;
case "east":
facing = new Vec3f(-1f, 0f, 0f);
return true;
case "west":
facing = new Vec3f(1f, 0f, 0f);
return true;
case "up":
facing = new Vec3f(0f, 1f, 0f);
return true;
case "down":
facing = new Vec3f(0f, -1f, 0f);
return true;
default:
return false;
}
}
private static void BuildBasisFromForward(Vec3f forward, out Vec3f right, out Vec3f up)
{
up = new Vec3f(0f, 1f, 0f);
if (Math.Abs(forward.Y) > 0.5f)
{
up = forward.Y > 0f ? new Vec3f(0f, 0f, -1f) : new Vec3f(0f, 0f, 1f);
right = new Vec3f(1f, 0f, 0f);
return;
}
right = new Vec3f(-forward.Z, 0f, forward.X);
}
private static void NormalizeFacing(ref Vec3f facing)
{
float length = MathF.Sqrt(facing.X * facing.X + facing.Y * facing.Y + facing.Z * facing.Z);
if (length <= 0.0001f)
{
facing = new Vec3f(0f, 0f, 1f);
return;
}
float invLength = 1f / length;
facing = new Vec3f(facing.X * invLength, facing.Y * invLength, facing.Z * invLength);
}
private static bool TryGetMeshAngle(BlockEntity blockEntity, out float radians)
{
if (blockEntity is BlockEntityGenericTypedContainer genericContainer)
{
radians = genericContainer.MeshAngle;
return true;
}
if (blockEntity is BlockEntityCrate crate)
{
radians = crate.MeshAngle;
return true;
}
radians = 0f;
return false;
}
}

View File

@@ -19,7 +19,7 @@ internal class WorldBillboardRenderer(ICoreClientAPI api, Config config, CardRen
private readonly List<PreviewTarget> frameTargets = [];
private readonly CardRenderer cardRenderer = cardRenderer;
private readonly PreviewTargetProvider targetProvider = new(api, config);
private readonly WorldBillboardPresenter worldBillboardPresenter = new(api);
private readonly WorldBillboardPresenter worldBillboardPresenter = new(api, config);
private readonly MeshRef quadMeshRef = api.Render.UploadMesh(
QuadMeshUtil.GetCustomQuadModelData(
1f,

View File

@@ -27,6 +27,13 @@
"default": true,
"clientSide": true
},
{
"code": "BillboardIgnoreFront",
"comment": "config-desc-BillboardIgnoreFront",
"type": "boolean",
"default": false,
"clientSide": true
},
{
"code": "BillboardColumnsPerBlock",
"comment": "config-desc-BillboardColumnsPerBlock",

View File

@@ -3,6 +3,7 @@
"hotkey-preview-containers-nearby": "Preview containers nearby",
"config-desc-Mode": "Preview mode. Valid values: None, UnderCursor, OnHoveredContainer, OnNearbyContainers.",
"config-desc-HoldKey": "Previews only show while the \"Preview containers\" key is held.",
"config-desc-BillboardIgnoreFront": "With this set to false, in-world previews show on the front side of containers. By setting this to true, the previews show on the side determined to be most visible.",
"config-desc-BillboardColumnsPerBlock": "Columns per block width used for world billboards. For containers 2 blocks wide it's doubled.",
"config-desc-ColumnsUnderCursor": "Columns in the UnderCursor mode.",
"config-desc-PreviewNearbyRadius": "Radius for \"Preview containers nearby\".",

View File

@@ -7,7 +7,7 @@
"OrekiWoof"
],
"description": "see containers' contents without having to open them",
"version": "1.1.0",
"version": "1.2.0",
"dependencies": {
"game": "1.21.0"
},

View File

@@ -4,6 +4,9 @@
<TargetFramework>net8.0</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<Configurations>Debug;Release;Version22</Configurations>
<VS_CONFIGLIB Condition="'$(VS_CONFIGLIB)' == ''">$([System.Environment]::GetEnvironmentVariable('VS_CONFIGLIB'))</VS_CONFIGLIB>
<ConfigLibAvailable Condition="'$(VS_CONFIGLIB)' != '' and Exists('$(VS_CONFIGLIB)')">true</ConfigLibAvailable>
<DefineConstants Condition="'$(ConfigLibAvailable)' == 'true'">$(DefineConstants);CONFIGLIB</DefineConstants>
</PropertyGroup>
<ItemGroup>
@@ -17,4 +20,15 @@
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="configlib" Condition="'$(ConfigLibAvailable)' == 'true'">
<HintPath>$(VS_CONFIGLIB)</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<Target Name="WarnWithoutConfigLib" BeforeTargets="CoreCompile" Condition="'$(ConfigLibAvailable)' != 'true'">
<Warning Text="No VS_CONFIGLIB - will compile without supporting ConfigLib. Set VS_CONFIGLIB env var to a path that contains the configlib's dlls." />
</Target>
</Project>

0
ChestPreview/build.sh Normal file → Executable file
View File

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 OrekiWoof
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.