16 Commits

Author SHA1 Message Date
97b0d0b0de version 2.0.0 2026-05-23 02:28:09 +02:00
78eeb3dca5 make toggle state persistent 2026-05-23 01:38:09 +02:00
05ae8bb276 hotkeys for toggles / simplify toggling mechanics 2026-05-23 01:13:30 +02:00
fb80e2ddbf Merge pull request 'Add toggle options for preview and preview nearby' (#4) from Spoonail/ChestPreview:option-toggle-preview into main 2026-05-22 19:27:05 +02:00
Mononospace
e835998390 Add toggle options for preview and preview nearby 2026-05-22 19:25:56 +02:00
8bf4735da7 update envs 2026-05-22 19:25:16 +02:00
79c6ed928a update version to 1.2.0 2026-03-28 04:51:11 +01:00
b912a664e7 Merge pull request 'config option for ignoring fronts of containers' (#2) from side-view-all into main 2026-03-28 04:50:43 +01:00
b23345b273 config option for ignoring fronts of containers 2026-03-28 04:49:19 +01:00
1e20533f10 add MIT license 2026-03-16 15:17:30 +01:00
2ad99a9d3b VS_CONFIGLIB for CakeBuild 2026-03-16 00:37:10 +01:00
074b34dd24 crash prevention when failing to GetContents in CardRenderer 2026-03-16 00:36:49 +01:00
d7c541fcc5 front_face_offset 0.01 -> 0.05 2026-03-16 00:01:10 +01:00
16b901fab7 fix rendering on the opposite side when reading "side" 2026-03-15 23:55:21 +01:00
5389901c82 support for Forge 2026-03-15 23:53:23 +01:00
27cfbcaa10 support for groundstorable containers and bloomery 2026-03-15 23:24:44 +01:00
17 changed files with 1660 additions and 1280 deletions

View File

@@ -1,5 +1,5 @@
{ {
"total": 66247, "total": 90094,
"sessions": [ "sessions": [
{ {
"begin": "2026-03-11T23:50:47+01:00", "begin": "2026-03-11T23:50:47+01:00",
@@ -65,6 +65,91 @@
"begin": "2026-03-15T20:33:08+01:00", "begin": "2026-03-15T20:33:08+01:00",
"end": "2026-03-15T20:53:09+01:00", "end": "2026-03-15T20:53:09+01:00",
"duration": 1201 "duration": 1201
},
{
"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
},
{
"begin": "2026-05-22T17:40:18+02:00",
"end": "2026-05-22T18:03:45+02:00",
"duration": 1406
},
{
"begin": "2026-05-22T18:30:12+02:00",
"end": "2026-05-22T18:34:35+02:00",
"duration": 262
},
{
"begin": "2026-05-22T18:34:49+02:00",
"end": "2026-05-22T18:38:39+02:00",
"duration": 229
},
{
"begin": "2026-05-22T18:38:55+02:00",
"end": "2026-05-22T18:39:22+02:00",
"duration": 27
},
{
"begin": "2026-05-22T18:58:35+02:00",
"end": "2026-05-22T19:03:47+02:00",
"duration": 311
},
{
"begin": "2026-05-22T19:03:52+02:00",
"end": "2026-05-22T19:05:29+02:00",
"duration": 96
},
{
"begin": "2026-05-22T19:05:33+02:00",
"end": "2026-05-22T19:12:57+02:00",
"duration": 444
},
{
"begin": "2026-05-22T19:13:27+02:00",
"end": "2026-05-22T19:21:36+02:00",
"duration": 489
},
{
"begin": "2026-05-22T19:22:06+02:00",
"end": "2026-05-22T19:47:51+02:00",
"duration": 1545
},
{
"begin": "2026-05-22T20:11:03+02:00",
"end": "2026-05-22T20:42:49+02:00",
"duration": 1906
},
{
"begin": "2026-05-23T00:13:03+02:00",
"end": "2026-05-23T00:33:04+02:00",
"duration": 1201
},
{
"begin": "2026-05-23T00:34:04+02:00",
"end": "2026-05-23T01:10:47+02:00",
"duration": 2202
},
{
"begin": "2026-05-23T01:10:47+02:00",
"end": "2026-05-23T01:38:03+02:00",
"duration": 1636
},
{
"begin": "2026-05-23T01:38:05+02:00",
"end": "2026-05-23T02:04:35+02:00",
"duration": 1590
},
{
"begin": "2026-05-23T02:05:09+02:00",
"end": "2026-05-23T02:27:49+02:00",
"duration": 1360
} }
] ]
} }

4
.vscode/tasks.json vendored
View File

@@ -4,9 +4,9 @@
{ {
"label": "check env", "label": "check env",
"type": "shell", "type": "shell",
"command": "[ -n \"$VINTAGE_STORY\" ] || { echo 'ERROR: VINTAGE_STORY is not set'; exit 1; }; [ -n \"$VINTAGE_STORY_DATA\" ] || { echo 'ERROR: VINTAGE_STORY_DATA is not set'; exit 1; }", "command": "if [ \"${input:buildConfig}\" = \"Version22\" ]; then [ -n \"$VINTAGE_STORY_22\" ] || { echo 'ERROR: VINTAGE_STORY_22 is not set'; exit 1; }; [ -n \"$VINTAGE_STORY_DATA_22\" ] || { echo 'ERROR: VINTAGE_STORY_DATA_22 is not set'; exit 1; }; else [ -n \"$VINTAGE_STORY_21\" ] || { echo 'ERROR: VINTAGE_STORY_21 is not set'; exit 1; }; [ -n \"$VINTAGE_STORY_DATA_21\" ] || { echo 'ERROR: VINTAGE_STORY_DATA_21 is not set'; exit 1; }; fi",
"windows": { "windows": {
"command": "if (-not $env:VINTAGE_STORY) { Write-Host 'ERROR: VINTAGE_STORY is not set'; exit 1 }; if (-not $env:VINTAGE_STORY_DATA) { Write-Host 'ERROR: VINTAGE_STORY_DATA is not set'; exit 1 }" "command": "if ('${input:buildConfig}' -eq 'Version22') { if (-not $env:VINTAGE_STORY_22) { Write-Host 'ERROR: VINTAGE_STORY_22 is not set'; exit 1 }; if (-not $env:VINTAGE_STORY_DATA_22) { Write-Host 'ERROR: VINTAGE_STORY_DATA_22 is not set'; exit 1 } } else { if (-not $env:VINTAGE_STORY_21) { Write-Host 'ERROR: VINTAGE_STORY_21 is not set'; exit 1 }; if (-not $env:VINTAGE_STORY_DATA_21) { Write-Host 'ERROR: VINTAGE_STORY_DATA_21 is not set'; exit 1 } }"
}, },
"problemMatcher": [] "problemMatcher": []
}, },

View File

@@ -6,6 +6,8 @@
<OutputPath>bin\$(Configuration)\Mods\mod</OutputPath> <OutputPath>bin\$(Configuration)\Mods\mod</OutputPath>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Version22</Configurations> <Configurations>Debug;Release;Version22</Configurations>
<VINTAGE_STORY>$(VINTAGE_STORY_21)</VINTAGE_STORY>
<VINTAGE_STORY_DATA>$(VINTAGE_STORY_DATA_21)</VINTAGE_STORY_DATA>
<VS_CONFIGLIB Condition="'$(VS_CONFIGLIB)' == ''">$([System.Environment]::GetEnvironmentVariable('VS_CONFIGLIB'))</VS_CONFIGLIB> <VS_CONFIGLIB Condition="'$(VS_CONFIGLIB)' == ''">$([System.Environment]::GetEnvironmentVariable('VS_CONFIGLIB'))</VS_CONFIGLIB>
<ConfigLibAvailable Condition="'$(VS_CONFIGLIB)' != '' and Exists('$(VS_CONFIGLIB)')">true</ConfigLibAvailable> <ConfigLibAvailable Condition="'$(VS_CONFIGLIB)' != '' and Exists('$(VS_CONFIGLIB)')">true</ConfigLibAvailable>
<DefineConstants Condition="'$(ConfigLibAvailable)' == 'true'">$(DefineConstants);CONFIGLIB</DefineConstants> <DefineConstants Condition="'$(ConfigLibAvailable)' == 'true'">$(DefineConstants);CONFIGLIB</DefineConstants>

View File

@@ -4,7 +4,7 @@ public sealed class Config
{ {
public string Mode { get; set; } = PreviewModes.UNDER_CURSOR; public string Mode { get; set; } = PreviewModes.UNDER_CURSOR;
public bool HoldKey { get; set; } = true; public bool BillboardIgnoreFront { get; set; } = false;
public int BillboardColumnsPerBlock { get; set; } = 4; public int BillboardColumnsPerBlock { get; set; } = 4;
@@ -14,7 +14,9 @@ public sealed class Config
public bool WhitelistedContainersOnly { get; set; } = true; public bool WhitelistedContainersOnly { get; set; } = true;
public string WhitelistedContainers { get; set; } = "barrel, crate, chest-*, trunk-*, storagevessel-*, stationarybasket-*, labeledchest-*"; public string WhitelistedContainers { get; set; } = "barrel, crate, chest-*, trunk-*, storagevessel-*, stationarybasket-*, labeledchest-*, groundstorage";
public string BlacklistedContainers { get; set; } = ""; public string BlacklistedContainers { get; set; } = "";
public bool GroundStorageOnlyContainers { get; set; } = true;
} }

View File

@@ -0,0 +1,7 @@
namespace ChestPreview.Configs;
public class State
{
public bool PreviewNearbyToggleActive { get; set; } = false;
public bool PreviewToggleActive { get; set; } = false;
}

View File

@@ -10,9 +10,12 @@ namespace ChestPreview.Core;
public class ChestPreviewModSystem : ModSystem public class ChestPreviewModSystem : ModSystem
{ {
internal const string CONFIG_FILENAME = "chestpreview.json"; internal const string CONFIG_FILENAME = "chestpreview.json";
internal const string STATE_FILENAME = "chestpreview-state.json";
internal const string CONFIGLIB_DOMAIN = "chestpreview"; internal const string CONFIGLIB_DOMAIN = "chestpreview";
internal const string PREVIEW_CONTAINERS_HOTKEY_CODE = "chestpreview-preview-containers"; internal const string PREVIEW_CONTAINERS_HOTKEY_CODE = "chestpreview-preview-containers";
internal const string PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE = "chestpreview-preview-containers-nearby"; internal const string PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE = "chestpreview-preview-containers-nearby";
internal const string TOGGLE_PREVIEW_CONTAINERS_HOTKEY_CODE = "chestpreview-preview-containers-toggle";
internal const string TOGGLE_PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE = "chestpreview-preview-containers-nearby-toggle";
private Config config = new(); private Config config = new();
private CardRenderer? cardRenderer; private CardRenderer? cardRenderer;
@@ -20,6 +23,8 @@ public class ChestPreviewModSystem : ModSystem
private StorageHoverHudRenderer? storageHoverHudRenderer; private StorageHoverHudRenderer? storageHoverHudRenderer;
private WorldBillboardRenderer? worldBillboardRenderer; private WorldBillboardRenderer? worldBillboardRenderer;
public State State { get; } = new();
public override bool ShouldLoad(EnumAppSide forSide) => forSide is EnumAppSide.Client; public override bool ShouldLoad(EnumAppSide forSide) => forSide is EnumAppSide.Client;
public override void StartClientSide(ICoreClientAPI api) public override void StartClientSide(ICoreClientAPI api)
@@ -35,6 +40,24 @@ public class ChestPreviewModSystem : ModSystem
worldBillboardRenderer = new WorldBillboardRenderer(api, config, cardRenderer); worldBillboardRenderer = new WorldBillboardRenderer(api, config, cardRenderer);
api.Event.RegisterRenderer(worldBillboardRenderer, EnumRenderStage.AfterOIT, "chestpreview-world-billboard-preview"); api.Event.RegisterRenderer(worldBillboardRenderer, EnumRenderStage.AfterOIT, "chestpreview-world-billboard-preview");
api.Input.SetHotKeyHandler(
hotkeyCode: TOGGLE_PREVIEW_CONTAINERS_HOTKEY_CODE,
keycomb =>
{
State.PreviewToggleActive = !State.PreviewToggleActive;
api.StoreModConfig(State, STATE_FILENAME);
return true;
});
api.Input.SetHotKeyHandler(
hotkeyCode: TOGGLE_PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE,
keycomb =>
{
State.PreviewNearbyToggleActive = !State.PreviewNearbyToggleActive;
api.StoreModConfig(State, STATE_FILENAME);
return true;
});
} }
public override void Dispose() public override void Dispose()
@@ -63,10 +86,14 @@ public class ChestPreviewModSystem : ModSystem
public Config LoadClientConfig(ICoreClientAPI api) public Config LoadClientConfig(ICoreClientAPI api)
{ {
Config config; Config config;
State state;
try try
{ {
config = api.LoadModConfig<Config>(CONFIG_FILENAME) ?? new Config(); config = api.LoadModConfig<Config>(CONFIG_FILENAME) ?? new Config();
state = api.LoadModConfig<State>(STATE_FILENAME) ?? new State();
State.PreviewNearbyToggleActive = state.PreviewNearbyToggleActive;
State.PreviewToggleActive = state.PreviewToggleActive;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -85,6 +112,7 @@ public class ChestPreviewModSystem : ModSystem
config.PreviewNearbyRadius = config.PreviewNearbyRadius; config.PreviewNearbyRadius = config.PreviewNearbyRadius;
api.StoreModConfig(config, CONFIG_FILENAME); api.StoreModConfig(config, CONFIG_FILENAME);
api.StoreModConfig(State, STATE_FILENAME);
return config; return config;
} }
@@ -107,5 +135,23 @@ public class ChestPreviewModSystem : ModSystem
altPressed: false, altPressed: false,
ctrlPressed: true, ctrlPressed: true,
shiftPressed: false); shiftPressed: false);
api.Input.RegisterHotKey(
hotkeyCode: TOGGLE_PREVIEW_CONTAINERS_HOTKEY_CODE,
name: Lang.Get("chestpreview:hotkey-preview-container-toggle"),
key: GlKeys.Unknown,
type: HotkeyType.HelpAndOverlays,
altPressed: false,
ctrlPressed: false,
shiftPressed: false);
api.Input.RegisterHotKey(
hotkeyCode: TOGGLE_PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE,
name: Lang.Get("chestpreview:hotkey-preview-containers-nearby-toggle"),
key: GlKeys.Unknown,
type: HotkeyType.HelpAndOverlays,
altPressed: false,
ctrlPressed: false,
shiftPressed: false);
} }
} }

View File

@@ -11,6 +11,7 @@ using Vintagestory.API.Client;
using Vintagestory.API.Common; using Vintagestory.API.Common;
using Vintagestory.API.MathTools; using Vintagestory.API.MathTools;
using Vintagestory.API.Util; using Vintagestory.API.Util;
using Vintagestory.GameContent;
namespace ChestPreview; namespace ChestPreview;
@@ -23,6 +24,7 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
private readonly Config config = config; private readonly Config config = config;
private readonly List<BlockEntity> nearbyContainerEntities = []; private readonly List<BlockEntity> nearbyContainerEntities = [];
private float nearbyScanAccumulator; private float nearbyScanAccumulator;
private readonly Lazy<State> state = new(() => api.ModLoader.GetModSystem<ChestPreviewModSystem>().State);
public void CollectTargets(float deltaTime, List<PreviewTarget> targets) public void CollectTargets(float deltaTime, List<PreviewTarget> targets)
{ {
@@ -39,7 +41,7 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
if (!TryGetHoveredContainer(out Block hoveredBlock, out BlockEntity hoveredBlockEntity)) if (!TryGetHoveredContainer(out Block hoveredBlock, out BlockEntity hoveredBlockEntity))
return; return;
if (!CanAccessContainer(hoveredBlockEntity)) if (!CanAccessContainer(hoveredBlock, hoveredBlockEntity))
return; return;
if (!IsContainerAllowed(hoveredBlock)) if (!IsContainerAllowed(hoveredBlock))
@@ -70,7 +72,7 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
if (block.Id == 0) if (block.Id == 0)
continue; continue;
if (!CanAccessContainer(blockEntity)) if (!CanAccessContainer(block, blockEntity))
continue; continue;
if (!IsContainerAllowed(block)) if (!IsContainerAllowed(block))
@@ -82,13 +84,16 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
private string GetActiveMode() private string GetActiveMode()
{ {
if (IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE)) var previewKeyHeld = IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_HOTKEY_CODE);
var previewNearbyKeyHeld = IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE);
if (state.Value.PreviewNearbyToggleActive || previewNearbyKeyHeld)
return PreviewModes.ON_NEARBY_CONTAINERS; return PreviewModes.ON_NEARBY_CONTAINERS;
if (config.HoldKey && !IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_HOTKEY_CODE)) if (state.Value.PreviewToggleActive || previewKeyHeld)
return PreviewModes.NONE; return PreviewModes.Normalize(config.Mode);
return PreviewModes.Normalize(config.Mode); return PreviewModes.NONE;
} }
private bool IsHotkeyHeld(string hotkeyCode) private bool IsHotkeyHeld(string hotkeyCode)
@@ -134,10 +139,14 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
if (currentBlockSelection == null) if (currentBlockSelection == null)
return false; return false;
BlockEntity? currentBlockEntity = api.World.BlockAccessor.GetBlockEntity(currentBlockSelection.Position); BlockPos hoveredPos = currentBlockSelection.Position;
if (currentBlockEntity is IBlockEntityContainer) BlockEntity? currentBlockEntity = api.World.BlockAccessor.GetBlockEntity(hoveredPos);
if (HasPreviewableStorage(currentBlockSelection.Block, hoveredPos, currentBlockEntity))
{ {
block = currentBlockSelection.Block; block = currentBlockSelection.Block;
if (currentBlockEntity == null)
return false;
blockEntity = currentBlockEntity; blockEntity = currentBlockEntity;
return true; return true;
} }
@@ -159,14 +168,17 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
return false; return false;
BlockPos controllerPos = multiblockOffset.GetControlBlockPos(blockSelection.Position); BlockPos controllerPos = multiblockOffset.GetControlBlockPos(blockSelection.Position);
BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(controllerPos);
if (blockEntity is not IBlockEntityContainer)
return false;
controllerBlock = api.World.BlockAccessor.GetBlock(controllerPos); controllerBlock = api.World.BlockAccessor.GetBlock(controllerPos);
if (controllerBlock.Id == 0) if (controllerBlock.Id == 0)
return false; return false;
BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(controllerPos);
if (!HasPreviewableStorage(controllerBlock, controllerPos, blockEntity))
return false;
if (blockEntity == null)
return false;
controllerBlockEntity = blockEntity; controllerBlockEntity = blockEntity;
return true; return true;
} }
@@ -196,11 +208,15 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
if (Math.Abs(y + 0.5d - playerEntity.CameraPos.Y) > nearbyRadius) if (Math.Abs(y + 0.5d - playerEntity.CameraPos.Y) > nearbyRadius)
return; return;
BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(new BlockPos(x, y, z)); if (block.Id == 0)
if (blockEntity is not IBlockEntityContainer)
return; return;
if (block.Id == 0) BlockPos blockPos = new(x, y, z);
BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(blockPos);
if (!HasPreviewableStorage(block, blockPos, blockEntity))
return;
if (blockEntity == null)
return; return;
nearbyContainerEntities.Add(blockEntity); nearbyContainerEntities.Add(blockEntity);
@@ -232,17 +248,52 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
} }
} }
private bool CanAccessContainer(BlockEntity blockEntity) private bool CanAccessContainer(Block block, BlockEntity blockEntity)
{ {
if (blockEntity is not IBlockEntityContainer container || container.Inventory is not InventoryBase inventory) if (config.GroundStorageOnlyContainers && blockEntity is BlockEntityGroundStorage && !HasHeldBagInGroundStorage(blockEntity))
return false; return false;
IPlayer? player = api.World.Player; BlockPos blockPos = blockEntity.Pos;
EntityPlayer? playerEntity = player?.Entity; if (block.GetInterface<IBlockEntityContainer>(api.World, blockPos) is { } container && container.Inventory is InventoryBase inventory)
if (player == null || playerEntity == null) {
IPlayer? player = api.World.Player;
EntityPlayer? playerEntity = player?.Entity;
if (player == null || playerEntity == null)
return false;
return inventory.CanPlayerAccess(player, playerEntity.GetPos());
}
if (blockEntity is BlockEntityBloomery or BlockEntityForge)
return true;
return HasHeldBagInGroundStorage(blockEntity);
}
private bool HasPreviewableStorage(Block block, BlockPos blockPos, BlockEntity? blockEntity)
{
if (config.GroundStorageOnlyContainers && blockEntity is BlockEntityGroundStorage)
return HasHeldBagInGroundStorage(blockEntity);
return block.GetInterface<IBlockEntityContainer>(api.World, blockPos) != null || HasHeldBagInGroundStorage(blockEntity) || (blockEntity is BlockEntityBloomery or BlockEntityForge);
}
private static bool HasHeldBagInGroundStorage(BlockEntity? blockEntity)
{
if (blockEntity is not BlockEntityGroundStorage groundStorage)
return false; return false;
return inventory.CanPlayerAccess(player, playerEntity.GetPos()); foreach (ItemSlot? slot in groundStorage.Inventory)
{
if (slot?.Itemstack?.Collectible is not { } collectible)
continue;
IHeldBag? heldBag = collectible.GetCollectibleInterface<IHeldBag>();
if (heldBag != null)
return true;
}
return false;
} }
private bool IsContainerAllowed(Block block) private bool IsContainerAllowed(Block block)

View File

@@ -4,12 +4,15 @@ using ChestPreview.Models;
using ChestPreview.Utils; using ChestPreview.Utils;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using Vintagestory.API.Client; using Vintagestory.API.Client;
using Vintagestory.API.Common; using Vintagestory.API.Common;
using Vintagestory.API.Config; using Vintagestory.API.Config;
using Vintagestory.API.Datastructures;
using Vintagestory.API.MathTools; using Vintagestory.API.MathTools;
using Vintagestory.GameContent;
namespace ChestPreview.Rendering; namespace ChestPreview.Rendering;
@@ -28,6 +31,8 @@ internal class CardRenderer(ICoreClientAPI api, Config config) : IDisposable
private readonly ICoreClientAPI api = api; private readonly ICoreClientAPI api = api;
private readonly Config config = config; private readonly Config config = config;
private readonly TreeAttribute tempTree = new();
private readonly InventoryGeneric sharedPreviewInventory = new(64, "inventory-preview", null, null);
private readonly Dictionary<string, CachedCardTexture> cardTextureByTarget = []; private readonly Dictionary<string, CachedCardTexture> cardTextureByTarget = [];
private readonly Dictionary<int, LoadedTexture> atlasTextureById = []; private readonly Dictionary<int, LoadedTexture> atlasTextureById = [];
private readonly Dictionary<string, RenderedIconEntry> renderedIconByStack = []; private readonly Dictionary<string, RenderedIconEntry> renderedIconByStack = [];
@@ -46,9 +51,33 @@ internal class CardRenderer(ICoreClientAPI api, Config config) : IDisposable
{ {
texture = null!; texture = null!;
if (target.BlockEntity is not IBlockEntityContainer container || container.Inventory is not InventoryBase inventory) if (TryResolveHeldBagContents(target.BlockEntity, out List<ItemStack>? bagContents))
{
string bagTargetKey = CreateTargetKey(target.BlockEntity.Pos);
if (!cardTextureByTarget.TryGetValue(bagTargetKey, out CachedCardTexture? bagCachedTexture))
{
bagCachedTexture = new CachedCardTexture(new LoadedTexture(api), string.Empty);
cardTextureByTarget[bagTargetKey] = bagCachedTexture;
}
BuildLayoutFromStacks(bagContents, maxColumns, out List<GroupedItemCell> bagCells, out int bagUsedColumns, out int bagUsedRows, out string bagFingerprint);
if (bagCachedTexture.Fingerprint != bagFingerprint)
{
ComposeTexture(bagCachedTexture.Texture, bagCells, bagUsedColumns, bagUsedRows);
bagCachedTexture.Fingerprint = bagFingerprint;
}
texture = bagCachedTexture.Texture;
return texture.TextureId != 0;
}
if (config.GroundStorageOnlyContainers && target.BlockEntity is BlockEntityGroundStorage)
return false; return false;
if (!TryResolveInventory(target, out InventoryBase? resolvedInventory))
return false;
InventoryBase inventory = resolvedInventory;
IPlayer? player = api.World.Player; IPlayer? player = api.World.Player;
EntityPlayer? playerEntity = player?.Entity; EntityPlayer? playerEntity = player?.Entity;
if (player == null || playerEntity == null || !inventory.CanPlayerAccess(player, playerEntity.GetPos())) if (player == null || playerEntity == null || !inventory.CanPlayerAccess(player, playerEntity.GetPos()))
@@ -61,17 +90,111 @@ internal class CardRenderer(ICoreClientAPI api, Config config) : IDisposable
cardTextureByTarget[targetKey] = cachedTexture; cardTextureByTarget[targetKey] = cachedTexture;
} }
BuildLayout(inventory, maxColumns, out List<GroupedItemCell> cells, out int usedColumns, out int usedRows, out string fingerprint); BuildLayout(inventory, maxColumns, out List<GroupedItemCell> cellsFromInventory, out int usedColumnsFromInventory, out int usedRowsFromInventory, out string fingerprintFromInventory);
if (cachedTexture.Fingerprint != fingerprint) if (cachedTexture.Fingerprint != fingerprintFromInventory)
{ {
ComposeTexture(cachedTexture.Texture, cells, usedColumns, usedRows); ComposeTexture(cachedTexture.Texture, cellsFromInventory, usedColumnsFromInventory, usedRowsFromInventory);
cachedTexture.Fingerprint = fingerprint; cachedTexture.Fingerprint = fingerprintFromInventory;
} }
texture = cachedTexture.Texture; texture = cachedTexture.Texture;
return texture.TextureId != 0; return texture.TextureId != 0;
} }
private bool TryResolveHeldBagContents(BlockEntity blockEntity, [NotNullWhen(true)] out List<ItemStack>? contents)
{
contents = null;
if (blockEntity is not BlockEntityGroundStorage groundStorage)
return false;
foreach (ItemSlot? slot in groundStorage.Inventory)
{
ItemStack? bagStack = slot?.Itemstack;
if (bagStack?.Collectible is not { } collectible)
continue;
IHeldBag? heldBag = collectible.GetCollectibleInterface<IHeldBag>();
if (heldBag == null)
continue;
ItemStack[]? bagContents;
try
{
bagContents = heldBag.GetContents(bagStack, api.World);
}
catch
{
continue;
}
if (bagContents == null)
continue;
contents = [];
foreach (ItemStack contentStack in bagContents)
{
if (contentStack == null || contentStack.StackSize <= 0)
continue;
contents.Add(contentStack);
}
return true;
}
return false;
}
private bool TryResolveInventory(PreviewTarget target, [NotNullWhen(true)] out InventoryBase? inventory)
{
inventory = null;
if (TryResolveSpecialInventory(target.BlockEntity, out InventoryBase? specialInventory))
{
inventory = specialInventory;
return true;
}
BlockPos pos = target.BlockEntity.Pos;
if (target.Block.GetInterface<IBlockEntityContainer>(api.World, pos) is { } container && container.Inventory is InventoryBase blockInventory)
{
inventory = blockInventory;
return true;
}
return false;
}
private bool TryResolveSpecialInventory(BlockEntity blockEntity, [NotNullWhen(true)] out InventoryBase? inventory)
{
inventory = null;
if (blockEntity is not BlockEntityBloomery and not BlockEntityForge)
return false;
tempTree.Clear();
blockEntity.ToTreeAttributes(tempTree);
ITreeAttribute sourceTree = tempTree;
if (blockEntity is BlockEntityForge)
{
ITreeAttribute? forgeInventoryTree = tempTree.GetTreeAttribute("inventory");
if (forgeInventoryTree != null)
sourceTree = forgeInventoryTree;
}
sharedPreviewInventory.FromTreeAttributes(sourceTree);
if (blockEntity is BlockEntityForge && sharedPreviewInventory[0].Itemstack == null)
{
ItemStack? legacyContents = tempTree.GetItemstack("contents");
if (legacyContents != null)
sharedPreviewInventory[0].Itemstack = legacyContents;
}
inventory = sharedPreviewInventory;
return true;
}
public void DrawAtScreen(float x, float y, LoadedTexture texture) public void DrawAtScreen(float x, float y, LoadedTexture texture)
{ {
if (texture.TextureId == 0) if (texture.TextureId == 0)
@@ -105,16 +228,29 @@ internal class CardRenderer(ICoreClientAPI api, Config config) : IDisposable
} }
private void BuildLayout(IInventory inventory, int maxColumns, out List<GroupedItemCell> cells, out int usedColumns, out int usedRows, out string fingerprint) private void BuildLayout(IInventory inventory, int maxColumns, out List<GroupedItemCell> cells, out int usedColumns, out int usedRows, out string fingerprint)
{
List<ItemStack> stacks = [];
foreach (ItemSlot? slot in inventory)
{
if (slot?.Itemstack == null || slot.Empty)
continue;
stacks.Add(slot.Itemstack);
}
BuildLayoutFromStacks(stacks, maxColumns, out cells, out usedColumns, out usedRows, out fingerprint);
}
private void BuildLayoutFromStacks(IEnumerable<ItemStack> stacks, int maxColumns, out List<GroupedItemCell> cells, out int usedColumns, out int usedRows, out string fingerprint)
{ {
List<GroupedItemCell> groupedItems = []; List<GroupedItemCell> groupedItems = [];
int naturalOrder = 0; int naturalOrder = 0;
foreach (ItemSlot? slot in inventory) foreach (ItemStack stack in stacks)
{ {
if (slot == null || slot.Empty || slot.Itemstack == null) if (stack == null || stack.StackSize <= 0)
continue; continue;
ItemStack stack = slot.Itemstack;
int existingIndex = FindMatchingItemIndex(groupedItems, stack); int existingIndex = FindMatchingItemIndex(groupedItems, stack);
if (existingIndex >= 0) if (existingIndex >= 0)
{ {

View File

@@ -11,12 +11,13 @@ using Vintagestory.GameContent;
namespace ChestPreview.Rendering; namespace ChestPreview.Rendering;
internal class WorldBillboardPresenter(ICoreClientAPI api) internal class WorldBillboardPresenter(ICoreClientAPI api, Config config)
{ {
private const int multiblock_scan_radius = 2; private const int multiblock_scan_radius = 2;
private static readonly float front_face_offset = 0.01f; private static readonly float front_face_offset = 0.05f;
private readonly ICoreClientAPI api = api; private readonly ICoreClientAPI api = api;
private readonly Config config = config;
private readonly List<BillboardTarget> frameBillboards = []; private readonly List<BillboardTarget> frameBillboards = [];
public IReadOnlyList<BillboardTarget> FrameBillboards => frameBillboards; public IReadOnlyList<BillboardTarget> FrameBillboards => frameBillboards;
@@ -162,7 +163,7 @@ internal class WorldBillboardPresenter(ICoreClientAPI api)
right = new Vec3f(1f, 0f, 0f); right = new Vec3f(1f, 0f, 0f);
up = new Vec3f(0f, 1f, 0f); up = new Vec3f(0f, 1f, 0f);
if (TryGetMeshAngle(blockEntity, out float meshAngleRadians)) if (!config.BillboardIgnoreFront && TryGetMeshAngle(blockEntity, out float meshAngleRadians))
{ {
// MeshAngle is around Y axis in radians. // MeshAngle is around Y axis in radians.
forward = new Vec3f(MathF.Sin(meshAngleRadians), 0f, MathF.Cos(meshAngleRadians)); forward = new Vec3f(MathF.Sin(meshAngleRadians), 0f, MathF.Cos(meshAngleRadians));
@@ -171,7 +172,7 @@ internal class WorldBillboardPresenter(ICoreClientAPI api)
return true; return true;
} }
if (TryGetBlockSideFacing(block, out forward)) if (!config.BillboardIgnoreFront && TryGetBlockSideFacing(block, out forward))
{ {
BuildBasisFromForward(forward, out right, out up); BuildBasisFromForward(forward, out right, out up);
return true; return true;
@@ -260,16 +261,16 @@ internal class WorldBillboardPresenter(ICoreClientAPI api)
switch (side.ToLowerInvariant()) switch (side.ToLowerInvariant())
{ {
case "north": case "north":
facing = new Vec3f(0f, 0f, -1f);
return true;
case "south":
facing = new Vec3f(0f, 0f, 1f); facing = new Vec3f(0f, 0f, 1f);
return true; return true;
case "south":
facing = new Vec3f(0f, 0f, -1f);
return true;
case "east": case "east":
facing = new Vec3f(1f, 0f, 0f); facing = new Vec3f(-1f, 0f, 0f);
return true; return true;
case "west": case "west":
facing = new Vec3f(-1f, 0f, 0f); facing = new Vec3f(1f, 0f, 0f);
return true; return true;
case "up": case "up":
facing = new Vec3f(0f, 1f, 0f); facing = new Vec3f(0f, 1f, 0f);

View File

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

View File

@@ -21,10 +21,10 @@
"clientSide": true "clientSide": true
}, },
{ {
"code": "HoldKey", "code": "BillboardIgnoreFront",
"comment": "config-desc-HoldKey", "comment": "config-desc-BillboardIgnoreFront",
"type": "boolean", "type": "boolean",
"default": true, "default": false,
"clientSide": true "clientSide": true
}, },
{ {
@@ -72,7 +72,7 @@
"code": "WhitelistedContainers", "code": "WhitelistedContainers",
"comment": "config-desc-WhitelistedContainers", "comment": "config-desc-WhitelistedContainers",
"type": "string", "type": "string",
"default": "barrel, crate, chest-*, trunk-*, storagevessel-*, stationarybasket-*, labeledchest-*", "default": "barrel, crate, chest-*, trunk-*, storagevessel-*, stationarybasket-*, labeledchest-*, labeledtrunk-*, groundstorage, beehive-*",
"clientSide": true "clientSide": true
}, },
{ {
@@ -81,6 +81,13 @@
"type": "string", "type": "string",
"default": "", "default": "",
"clientSide": true "clientSide": true
},
{
"code": "GroundStorageOnlyContainers",
"comment": "config-desc-GroundStorageOnlyContainers",
"type": "boolean",
"default": true,
"clientSide": true
} }
] ]
} }

View File

@@ -1,12 +1,15 @@
{ {
"hotkey-preview-container": "Preview container", "hotkey-preview-container": "Preview container",
"hotkey-preview-containers-nearby": "Preview containers nearby", "hotkey-preview-containers-nearby": "Preview containers nearby",
"hotkey-preview-container-toggle": "Toggle preview container",
"hotkey-preview-containers-nearby-toggle": "Toggle preview on containers nearby",
"config-desc-Mode": "Preview mode. Valid values: None, UnderCursor, OnHoveredContainer, OnNearbyContainers.", "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-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-ColumnsUnderCursor": "Columns in the UnderCursor mode.",
"config-desc-PreviewNearbyRadius": "Radius for \"Preview containers nearby\".", "config-desc-PreviewNearbyRadius": "Radius for \"Preview containers nearby\".",
"config-desc-WhitelistedContainersOnly": "If true, only container codes in WhitelistedContainers are handled.", "config-desc-WhitelistedContainersOnly": "If true, only container codes in WhitelistedContainers are handled.",
"config-desc-GroundStorageOnlyContainers": "If true, groundstorage previews only appear when the stored item has its own inventory (for example bags/backpacks).",
"config-desc-WhitelistedContainers": "Allowed container codes, separated by comma, semicolon, or spaces. No effectif WhitelistedContainersOnly==false.", "config-desc-WhitelistedContainers": "Allowed container codes, separated by comma, semicolon, or spaces. No effectif WhitelistedContainersOnly==false.",
"config-desc-BlacklistedContainers": "Blocked container codes, separated by comma, semicolon, or spaces." "config-desc-BlacklistedContainers": "Blocked container codes, separated by comma, semicolon, or spaces."
} }

View File

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

View File

@@ -4,6 +4,14 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<Configurations>Debug;Release;Version22</Configurations> <Configurations>Debug;Release;Version22</Configurations>
<VINTAGE_STORY>$(VINTAGE_STORY_21)</VINTAGE_STORY>
<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>
<PropertyGroup Condition="'$(Configuration)' == 'Version22'">
<VINTAGE_STORY>$(VINTAGE_STORY_22)</VINTAGE_STORY>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -17,4 +25,15 @@
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath> <HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </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> </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.

View File

@@ -9,8 +9,8 @@
### Required env vars ### Required env vars
- `VINTAGE_STORY`: path to the 1.21 game install (on Windows default is %appdata%/VintageStory) - `VINTAGE_STORY_21`: path to the 1.21 game install (on Windows default is %appdata%/VintageStory)
- `VINTAGE_STORY_DATA`: path to the 1.21 game data directory (on Windows default is %appdata%/VintageStoryData) - `VINTAGE_STORY_DATA_21`: path to the 1.21 game data directory (on Windows default is %appdata%/VintageStoryData)
- `VINTAGE_STORY_22`: for 1.22 (`Version22` configuration) - `VINTAGE_STORY_22`: for 1.22 (`Version22` configuration)
- `VINTAGE_STORY_DATA_22`: for 1.22 (`Version22` configuration) - `VINTAGE_STORY_DATA_22`: for 1.22 (`Version22` configuration)
- `VS_CONFIGLIB`: path to `configlib.dll`. If not set, will compile with a warning and without configlib support - `VS_CONFIGLIB`: path to `configlib.dll`. If not set, will compile with a warning and without configlib support