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

@@ -1,20 +1,22 @@
namespace ChestPreview.Configs; namespace ChestPreview.Configs;
public sealed class Config 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;
public int ColumnsUnderCursor { get; set; } = 10; public int ColumnsUnderCursor { get; set; } = 10;
public int PreviewNearbyRadius { get; set; } = 8; public int PreviewNearbyRadius { get; set; } = 8;
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

@@ -1,111 +1,157 @@
using ChestPreview.Configs; using ChestPreview.Configs;
using ChestPreview.Rendering; using ChestPreview.Rendering;
using System; using System;
using Vintagestory.API.Client; using Vintagestory.API.Client;
using Vintagestory.API.Common; using Vintagestory.API.Common;
using Vintagestory.API.Config; using Vintagestory.API.Config;
namespace ChestPreview.Core; 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 CONFIGLIB_DOMAIN = "chestpreview"; internal const string STATE_FILENAME = "chestpreview-state.json";
internal const string PREVIEW_CONTAINERS_HOTKEY_CODE = "chestpreview-preview-containers"; internal const string CONFIGLIB_DOMAIN = "chestpreview";
internal const string PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE = "chestpreview-preview-containers-nearby"; internal const string PREVIEW_CONTAINERS_HOTKEY_CODE = "chestpreview-preview-containers";
internal const string PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE = "chestpreview-preview-containers-nearby";
private Config config = new(); internal const string TOGGLE_PREVIEW_CONTAINERS_HOTKEY_CODE = "chestpreview-preview-containers-toggle";
private CardRenderer? cardRenderer; internal const string TOGGLE_PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE = "chestpreview-preview-containers-nearby-toggle";
private ConfigLibBridge? configLibBridge;
private StorageHoverHudRenderer? storageHoverHudRenderer; private Config config = new();
private WorldBillboardRenderer? worldBillboardRenderer; private CardRenderer? cardRenderer;
private ConfigLibBridge? configLibBridge;
public override bool ShouldLoad(EnumAppSide forSide) => forSide is EnumAppSide.Client; private StorageHoverHudRenderer? storageHoverHudRenderer;
private WorldBillboardRenderer? worldBillboardRenderer;
public override void StartClientSide(ICoreClientAPI api)
{ public State State { get; } = new();
config = LoadClientConfig(api);
RegisterHotkeys(api); public override bool ShouldLoad(EnumAppSide forSide) => forSide is EnumAppSide.Client;
if (api.ModLoader.IsModEnabled(ConfigLibBridge.CONFIGLIB_MODID))
configLibBridge = ConfigLibBridge.TryCreate(api, config); public override void StartClientSide(ICoreClientAPI api)
cardRenderer = new CardRenderer(api, config); {
config = LoadClientConfig(api);
storageHoverHudRenderer = new StorageHoverHudRenderer(api, config, cardRenderer); RegisterHotkeys(api);
api.Event.RegisterRenderer(storageHoverHudRenderer, EnumRenderStage.Ortho, "chestpreview-storage-hover-hud"); if (api.ModLoader.IsModEnabled(ConfigLibBridge.CONFIGLIB_MODID))
configLibBridge = ConfigLibBridge.TryCreate(api, config);
worldBillboardRenderer = new WorldBillboardRenderer(api, config, cardRenderer); cardRenderer = new CardRenderer(api, config);
api.Event.RegisterRenderer(worldBillboardRenderer, EnumRenderStage.AfterOIT, "chestpreview-world-billboard-preview");
} storageHoverHudRenderer = new StorageHoverHudRenderer(api, config, cardRenderer);
api.Event.RegisterRenderer(storageHoverHudRenderer, EnumRenderStage.Ortho, "chestpreview-storage-hover-hud");
public override void Dispose()
{ worldBillboardRenderer = new WorldBillboardRenderer(api, config, cardRenderer);
configLibBridge?.Dispose(); api.Event.RegisterRenderer(worldBillboardRenderer, EnumRenderStage.AfterOIT, "chestpreview-world-billboard-preview");
configLibBridge = null;
api.Input.SetHotKeyHandler(
if (storageHoverHudRenderer is { } renderer) hotkeyCode: TOGGLE_PREVIEW_CONTAINERS_HOTKEY_CODE,
{ keycomb =>
renderer.Api.Event.UnregisterRenderer(renderer, EnumRenderStage.Ortho); {
renderer.Dispose(); State.PreviewToggleActive = !State.PreviewToggleActive;
storageHoverHudRenderer = null; api.StoreModConfig(State, STATE_FILENAME);
} return true;
});
if (worldBillboardRenderer is { } billboardRenderer)
{ api.Input.SetHotKeyHandler(
billboardRenderer.Api.Event.UnregisterRenderer(billboardRenderer, EnumRenderStage.AfterOIT); hotkeyCode: TOGGLE_PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE,
billboardRenderer.Dispose(); keycomb =>
worldBillboardRenderer = null; {
} State.PreviewNearbyToggleActive = !State.PreviewNearbyToggleActive;
api.StoreModConfig(State, STATE_FILENAME);
cardRenderer?.Dispose(); return true;
cardRenderer = null; });
} }
public Config LoadClientConfig(ICoreClientAPI api) public override void Dispose()
{ {
Config config; configLibBridge?.Dispose();
configLibBridge = null;
try
{ if (storageHoverHudRenderer is { } renderer)
config = api.LoadModConfig<Config>(CONFIG_FILENAME) ?? new Config(); {
} renderer.Api.Event.UnregisterRenderer(renderer, EnumRenderStage.Ortho);
catch (Exception ex) renderer.Dispose();
{ storageHoverHudRenderer = null;
Mod.Logger.Error("Error while loading config."); }
Mod.Logger.Error(ex);
config = new Config(); if (worldBillboardRenderer is { } billboardRenderer)
} {
billboardRenderer.Api.Event.UnregisterRenderer(billboardRenderer, EnumRenderStage.AfterOIT);
config.Mode = PreviewModes.Normalize(config.Mode); billboardRenderer.Dispose();
if (config.BillboardColumnsPerBlock <= 0) worldBillboardRenderer = null;
config.BillboardColumnsPerBlock = 5; }
if (config.ColumnsUnderCursor <= 0) cardRenderer?.Dispose();
config.ColumnsUnderCursor = 10; cardRenderer = null;
}
config.PreviewNearbyRadius = config.PreviewNearbyRadius;
public Config LoadClientConfig(ICoreClientAPI api)
api.StoreModConfig(config, CONFIG_FILENAME); {
return config; Config config;
} State state;
private static void RegisterHotkeys(ICoreClientAPI api) try
{ {
api.Input.RegisterHotKey( config = api.LoadModConfig<Config>(CONFIG_FILENAME) ?? new Config();
hotkeyCode: PREVIEW_CONTAINERS_HOTKEY_CODE, state = api.LoadModConfig<State>(STATE_FILENAME) ?? new State();
name: Lang.Get("chestpreview:hotkey-preview-container"), State.PreviewNearbyToggleActive = state.PreviewNearbyToggleActive;
key: GlKeys.LShift, State.PreviewToggleActive = state.PreviewToggleActive;
type: HotkeyType.HelpAndOverlays, }
altPressed: false, catch (Exception ex)
ctrlPressed: false, {
shiftPressed: false); Mod.Logger.Error("Error while loading config.");
Mod.Logger.Error(ex);
api.Input.RegisterHotKey( config = new Config();
hotkeyCode: PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE, }
name: Lang.Get("chestpreview:hotkey-preview-containers-nearby"),
key: GlKeys.LShift, config.Mode = PreviewModes.Normalize(config.Mode);
type: HotkeyType.HelpAndOverlays, if (config.BillboardColumnsPerBlock <= 0)
altPressed: false, config.BillboardColumnsPerBlock = 5;
ctrlPressed: true,
shiftPressed: false); if (config.ColumnsUnderCursor <= 0)
} config.ColumnsUnderCursor = 10;
}
config.PreviewNearbyRadius = config.PreviewNearbyRadius;
api.StoreModConfig(config, CONFIG_FILENAME);
api.StoreModConfig(State, STATE_FILENAME);
return config;
}
private static void RegisterHotkeys(ICoreClientAPI api)
{
api.Input.RegisterHotKey(
hotkeyCode: PREVIEW_CONTAINERS_HOTKEY_CODE,
name: Lang.Get("chestpreview:hotkey-preview-container"),
key: GlKeys.LShift,
type: HotkeyType.HelpAndOverlays,
altPressed: false,
ctrlPressed: false,
shiftPressed: false);
api.Input.RegisterHotKey(
hotkeyCode: PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE,
name: Lang.Get("chestpreview:hotkey-preview-containers-nearby"),
key: GlKeys.LShift,
type: HotkeyType.HelpAndOverlays,
altPressed: false,
ctrlPressed: true,
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

@@ -1,284 +1,335 @@
using ChestPreview.Configs; using ChestPreview.Configs;
using ChestPreview.Core; using ChestPreview.Core;
using ChestPreview.Models; using ChestPreview.Models;
using ChestPreview.Utils; using ChestPreview.Utils;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Vintagestory.API.Client; 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;
internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config) : IDisposable
{ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config) : IDisposable
private const float nearby_scan_interval_seconds = 0.1f; {
private static readonly Regex list_separator_regex = ListSeparatorRegex(); private const float nearby_scan_interval_seconds = 0.1f;
private static readonly Regex list_separator_regex = ListSeparatorRegex();
private readonly ICoreClientAPI api = api;
private readonly Config config = config; private readonly ICoreClientAPI api = api;
private readonly List<BlockEntity> nearbyContainerEntities = []; private readonly Config config = config;
private float nearbyScanAccumulator; private readonly List<BlockEntity> nearbyContainerEntities = [];
private float nearbyScanAccumulator;
public void CollectTargets(float deltaTime, List<PreviewTarget> targets) private readonly Lazy<State> state = new(() => api.ModLoader.GetModSystem<ChestPreviewModSystem>().State);
{
string mode = GetActiveMode(); public void CollectTargets(float deltaTime, List<PreviewTarget> targets)
if (mode == PreviewModes.NONE) {
return; string mode = GetActiveMode();
if (mode == PreviewModes.NONE)
if (mode == PreviewModes.ON_NEARBY_CONTAINERS) return;
{
CollectNearbyTargets(deltaTime, targets); if (mode == PreviewModes.ON_NEARBY_CONTAINERS)
return; {
} CollectNearbyTargets(deltaTime, targets);
return;
if (!TryGetHoveredContainer(out Block hoveredBlock, out BlockEntity hoveredBlockEntity)) }
return;
if (!TryGetHoveredContainer(out Block hoveredBlock, out BlockEntity hoveredBlockEntity))
if (!CanAccessContainer(hoveredBlockEntity)) return;
return;
if (!CanAccessContainer(hoveredBlock, hoveredBlockEntity))
if (!IsContainerAllowed(hoveredBlock)) return;
return;
if (!IsContainerAllowed(hoveredBlock))
targets.Add(new PreviewTarget(hoveredBlock, hoveredBlockEntity, mode, GetContainerAnchor(hoveredBlock, hoveredBlockEntity.Pos))); return;
}
targets.Add(new PreviewTarget(hoveredBlock, hoveredBlockEntity, mode, GetContainerAnchor(hoveredBlock, hoveredBlockEntity.Pos)));
public void Dispose() }
{
nearbyContainerEntities.Clear(); public void Dispose()
} {
nearbyContainerEntities.Clear();
private void CollectNearbyTargets(float deltaTime, List<PreviewTarget> targets) }
{
nearbyScanAccumulator += deltaTime; private void CollectNearbyTargets(float deltaTime, List<PreviewTarget> targets)
bool shouldRefresh = nearbyContainerEntities.Count == 0 || nearbyScanAccumulator >= nearby_scan_interval_seconds; {
nearbyScanAccumulator += deltaTime;
if (shouldRefresh) bool shouldRefresh = nearbyContainerEntities.Count == 0 || nearbyScanAccumulator >= nearby_scan_interval_seconds;
{
RefreshNearbyContainers(); if (shouldRefresh)
nearbyScanAccumulator = 0f; {
} RefreshNearbyContainers();
nearbyScanAccumulator = 0f;
foreach (BlockEntity blockEntity in nearbyContainerEntities) }
{
Block block = api.World.BlockAccessor.GetBlock(blockEntity.Pos); foreach (BlockEntity blockEntity in nearbyContainerEntities)
if (block.Id == 0) {
continue; Block block = api.World.BlockAccessor.GetBlock(blockEntity.Pos);
if (block.Id == 0)
if (!CanAccessContainer(blockEntity)) continue;
continue;
if (!CanAccessContainer(block, blockEntity))
if (!IsContainerAllowed(block)) continue;
continue;
if (!IsContainerAllowed(block))
targets.Add(new PreviewTarget(block, blockEntity, PreviewModes.ON_NEARBY_CONTAINERS, GetContainerAnchor(block, blockEntity.Pos))); continue;
}
} targets.Add(new PreviewTarget(block, blockEntity, PreviewModes.ON_NEARBY_CONTAINERS, GetContainerAnchor(block, blockEntity.Pos)));
}
private string GetActiveMode() }
{
if (IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE)) private string GetActiveMode()
return PreviewModes.ON_NEARBY_CONTAINERS; {
var previewKeyHeld = IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_HOTKEY_CODE);
if (config.HoldKey && !IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_HOTKEY_CODE)) var previewNearbyKeyHeld = IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_NEARBY_HOTKEY_CODE);
return PreviewModes.NONE;
if (state.Value.PreviewNearbyToggleActive || previewNearbyKeyHeld)
return PreviewModes.Normalize(config.Mode); return PreviewModes.ON_NEARBY_CONTAINERS;
}
if (state.Value.PreviewToggleActive || previewKeyHeld)
private bool IsHotkeyHeld(string hotkeyCode) return PreviewModes.Normalize(config.Mode);
{
HotKey? hotKey = api.Input.GetHotKeyByCode(hotkeyCode); return PreviewModes.NONE;
if (hotKey?.CurrentMapping == null) }
return false;
private bool IsHotkeyHeld(string hotkeyCode)
KeyCombination mapping = hotKey.CurrentMapping; {
if (!IsKeyHeld(mapping.KeyCode)) HotKey? hotKey = api.Input.GetHotKeyByCode(hotkeyCode);
return false; if (hotKey?.CurrentMapping == null)
return false;
if (mapping.SecondKeyCode.HasValue && !IsKeyHeld(mapping.SecondKeyCode.Value))
return false; KeyCombination mapping = hotKey.CurrentMapping;
if (!IsKeyHeld(mapping.KeyCode))
EntityControls? controls = api.World.Player?.Entity?.Controls; return false;
if (mapping.Ctrl && controls?.CtrlKey != true)
return false; if (mapping.SecondKeyCode.HasValue && !IsKeyHeld(mapping.SecondKeyCode.Value))
return false;
if (mapping.Shift && controls?.ShiftKey != true)
return false; EntityControls? controls = api.World.Player?.Entity?.Controls;
if (mapping.Ctrl && controls?.CtrlKey != true)
return !mapping.Alt || IsAltHeld(); return false;
}
if (mapping.Shift && controls?.ShiftKey != true)
private bool IsKeyHeld(int keyCode) return false;
{
bool[] keyStates = api.Input.KeyboardKeyStateRaw; return !mapping.Alt || IsAltHeld();
return keyCode >= 0 && keyCode < keyStates.Length && keyStates[keyCode]; }
}
private bool IsKeyHeld(int keyCode)
private bool IsAltHeld() {
{ bool[] keyStates = api.Input.KeyboardKeyStateRaw;
return IsKeyHeld((int)GlKeys.AltLeft) || IsKeyHeld((int)GlKeys.AltRight); return keyCode >= 0 && keyCode < keyStates.Length && keyStates[keyCode];
} }
private bool TryGetHoveredContainer(out Block block, out BlockEntity blockEntity) private bool IsAltHeld()
{ {
block = null!; return IsKeyHeld((int)GlKeys.AltLeft) || IsKeyHeld((int)GlKeys.AltRight);
blockEntity = null!; }
BlockSelection? currentBlockSelection = api.World.Player?.CurrentBlockSelection; private bool TryGetHoveredContainer(out Block block, out BlockEntity blockEntity)
if (currentBlockSelection == null) {
return false; block = null!;
blockEntity = null!;
BlockEntity? currentBlockEntity = api.World.BlockAccessor.GetBlockEntity(currentBlockSelection.Position);
if (currentBlockEntity is IBlockEntityContainer) BlockSelection? currentBlockSelection = api.World.Player?.CurrentBlockSelection;
{ if (currentBlockSelection == null)
block = currentBlockSelection.Block; return false;
blockEntity = currentBlockEntity;
return true; BlockPos hoveredPos = currentBlockSelection.Position;
} BlockEntity? currentBlockEntity = api.World.BlockAccessor.GetBlockEntity(hoveredPos);
if (HasPreviewableStorage(currentBlockSelection.Block, hoveredPos, currentBlockEntity))
if (!TryResolveMultiblockControllerContainer(currentBlockSelection, out Block controllerBlock, out BlockEntity controllerBlockEntity)) {
return false; block = currentBlockSelection.Block;
if (currentBlockEntity == null)
block = controllerBlock; return false;
blockEntity = controllerBlockEntity;
return true; blockEntity = currentBlockEntity;
} return true;
}
private bool TryResolveMultiblockControllerContainer(BlockSelection blockSelection, out Block controllerBlock, out BlockEntity controllerBlockEntity)
{ if (!TryResolveMultiblockControllerContainer(currentBlockSelection, out Block controllerBlock, out BlockEntity controllerBlockEntity))
controllerBlock = null!; return false;
controllerBlockEntity = null!;
block = controllerBlock;
if (blockSelection.Block is not IMultiblockOffset multiblockOffset) blockEntity = controllerBlockEntity;
return false; return true;
}
BlockPos controllerPos = multiblockOffset.GetControlBlockPos(blockSelection.Position);
BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(controllerPos); private bool TryResolveMultiblockControllerContainer(BlockSelection blockSelection, out Block controllerBlock, out BlockEntity controllerBlockEntity)
if (blockEntity is not IBlockEntityContainer) {
return false; controllerBlock = null!;
controllerBlockEntity = null!;
controllerBlock = api.World.BlockAccessor.GetBlock(controllerPos);
if (controllerBlock.Id == 0) if (blockSelection.Block is not IMultiblockOffset multiblockOffset)
return false; return false;
controllerBlockEntity = blockEntity; BlockPos controllerPos = multiblockOffset.GetControlBlockPos(blockSelection.Position);
return true; controllerBlock = api.World.BlockAccessor.GetBlock(controllerPos);
} if (controllerBlock.Id == 0)
return false;
private void RefreshNearbyContainers()
{ BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(controllerPos);
nearbyContainerEntities.Clear(); if (!HasPreviewableStorage(controllerBlock, controllerPos, blockEntity))
return false;
EntityPlayer? playerEntity = api.World.Player?.Entity;
if (playerEntity?.CameraPos == null) if (blockEntity == null)
return; return false;
float nearbyRadius = config.PreviewNearbyRadius; controllerBlockEntity = blockEntity;
float nearbyRadiusSquared = nearbyRadius * nearbyRadius; return true;
}
Vec3d nearbyRadiusOffset = new(nearbyRadius, nearbyRadius, nearbyRadius);
BlockPos minPos = (playerEntity.CameraPos - nearbyRadiusOffset).AsBlockPos; private void RefreshNearbyContainers()
BlockPos maxPos = (playerEntity.CameraPos + nearbyRadiusOffset + new Vec3d(1d, 1d, 1d)).AsBlockPos; {
nearbyContainerEntities.Clear();
api.World.BlockAccessor.WalkBlocks(minPos, maxPos, (block, x, y, z) =>
{ EntityPlayer? playerEntity = api.World.Player?.Entity;
double horizontalDx = x + 0.5d - playerEntity.CameraPos.X; if (playerEntity?.CameraPos == null)
double horizontalDz = z + 0.5d - playerEntity.CameraPos.Z; return;
if (horizontalDx * horizontalDx + horizontalDz * horizontalDz > nearbyRadiusSquared)
return; float nearbyRadius = config.PreviewNearbyRadius;
float nearbyRadiusSquared = nearbyRadius * nearbyRadius;
if (Math.Abs(y + 0.5d - playerEntity.CameraPos.Y) > nearbyRadius)
return; Vec3d nearbyRadiusOffset = new(nearbyRadius, nearbyRadius, nearbyRadius);
BlockPos minPos = (playerEntity.CameraPos - nearbyRadiusOffset).AsBlockPos;
BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(new BlockPos(x, y, z)); BlockPos maxPos = (playerEntity.CameraPos + nearbyRadiusOffset + new Vec3d(1d, 1d, 1d)).AsBlockPos;
if (blockEntity is not IBlockEntityContainer)
return; api.World.BlockAccessor.WalkBlocks(minPos, maxPos, (block, x, y, z) =>
{
if (block.Id == 0) double horizontalDx = x + 0.5d - playerEntity.CameraPos.X;
return; double horizontalDz = z + 0.5d - playerEntity.CameraPos.Z;
if (horizontalDx * horizontalDx + horizontalDz * horizontalDz > nearbyRadiusSquared)
nearbyContainerEntities.Add(blockEntity); return;
});
} if (Math.Abs(y + 0.5d - playerEntity.CameraPos.Y) > nearbyRadius)
return;
private Vec3d GetContainerAnchor(Block block, BlockPos blockPos)
{ if (block.Id == 0)
GetContainerBounds(block, blockPos, out Vector3 min, out Vector3 max); return;
Vector3 anchor = blockPos.ToVector3() + new Vector3((min.X + max.X) * 0.5f, max.Y + 0.1f, (min.Z + max.Z) * 0.5f); BlockPos blockPos = new(x, y, z);
return anchor.ToVec3d(); BlockEntity? blockEntity = api.World.BlockAccessor.GetBlockEntity(blockPos);
} if (!HasPreviewableStorage(block, blockPos, blockEntity))
return;
private void GetContainerBounds(Block block, BlockPos blockPos, out Vector3 min, out Vector3 max)
{ if (blockEntity == null)
Cuboidf[]? selectionBoxes = block.GetSelectionBoxes(api.World.BlockAccessor, blockPos); return;
min = new Vector3(0f, 0f, 0f); nearbyContainerEntities.Add(blockEntity);
max = new Vector3(1f, 1f, 1f); });
}
if (selectionBoxes is not { Length: > 0 })
return; private Vec3d GetContainerAnchor(Block block, BlockPos blockPos)
{
foreach (Cuboidf selectionBox in selectionBoxes) GetContainerBounds(block, blockPos, out Vector3 min, out Vector3 max);
{
min = Vector3.Min(min, selectionBox.Start.ToVector3()); Vector3 anchor = blockPos.ToVector3() + new Vector3((min.X + max.X) * 0.5f, max.Y + 0.1f, (min.Z + max.Z) * 0.5f);
max = Vector3.Max(max, selectionBox.End.ToVector3()); return anchor.ToVec3d();
} }
}
private void GetContainerBounds(Block block, BlockPos blockPos, out Vector3 min, out Vector3 max)
private bool CanAccessContainer(BlockEntity blockEntity) {
{ Cuboidf[]? selectionBoxes = block.GetSelectionBoxes(api.World.BlockAccessor, blockPos);
if (blockEntity is not IBlockEntityContainer container || container.Inventory is not InventoryBase inventory)
return false; min = new Vector3(0f, 0f, 0f);
max = new Vector3(1f, 1f, 1f);
IPlayer? player = api.World.Player;
EntityPlayer? playerEntity = player?.Entity; if (selectionBoxes is not { Length: > 0 })
if (player == null || playerEntity == null) return;
return false;
foreach (Cuboidf selectionBox in selectionBoxes)
return inventory.CanPlayerAccess(player, playerEntity.GetPos()); {
} min = Vector3.Min(min, selectionBox.Start.ToVector3());
max = Vector3.Max(max, selectionBox.End.ToVector3());
private bool IsContainerAllowed(Block block) }
{ }
string? code = block.Code?.Path;
if (string.IsNullOrWhiteSpace(code)) private bool CanAccessContainer(Block block, BlockEntity blockEntity)
return true; {
if (config.GroundStorageOnlyContainers && blockEntity is BlockEntityGroundStorage && !HasHeldBagInGroundStorage(blockEntity))
HashSet<string> blacklist = ParseContainerCodes(config.BlacklistedContainers); return false;
if (blacklist.Any(x => WildcardUtil.Match(x, code)))
return false; BlockPos blockPos = blockEntity.Pos;
if (block.GetInterface<IBlockEntityContainer>(api.World, blockPos) is { } container && container.Inventory is InventoryBase inventory)
if (!config.WhitelistedContainersOnly) {
return true; IPlayer? player = api.World.Player;
EntityPlayer? playerEntity = player?.Entity;
HashSet<string> whitelist = ParseContainerCodes(config.WhitelistedContainers); if (player == null || playerEntity == null)
return whitelist.Any(x => WildcardUtil.Match(x, code)); return false;
}
return inventory.CanPlayerAccess(player, playerEntity.GetPos());
private static HashSet<string> ParseContainerCodes(string rawList) }
{
if (string.IsNullOrWhiteSpace(rawList)) if (blockEntity is BlockEntityBloomery or BlockEntityForge)
return []; return true;
string[] entries = list_separator_regex.Split(rawList.Trim()); return HasHeldBagInGroundStorage(blockEntity);
HashSet<string> result = new(StringComparer.OrdinalIgnoreCase); }
foreach (string entry in entries) private bool HasPreviewableStorage(Block block, BlockPos blockPos, BlockEntity? blockEntity)
{ {
if (!string.IsNullOrWhiteSpace(entry)) if (config.GroundStorageOnlyContainers && blockEntity is BlockEntityGroundStorage)
result.Add(entry); return HasHeldBagInGroundStorage(blockEntity);
}
return block.GetInterface<IBlockEntityContainer>(api.World, blockPos) != null || HasHeldBagInGroundStorage(blockEntity) || (blockEntity is BlockEntityBloomery or BlockEntityForge);
return result; }
}
private static bool HasHeldBagInGroundStorage(BlockEntity? blockEntity)
[GeneratedRegex("[,;\\s]+", RegexOptions.Compiled)] {
private static partial Regex ListSeparatorRegex(); if (blockEntity is not BlockEntityGroundStorage groundStorage)
return false;
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)
{
string? code = block.Code?.Path;
if (string.IsNullOrWhiteSpace(code))
return true;
HashSet<string> blacklist = ParseContainerCodes(config.BlacklistedContainers);
if (blacklist.Any(x => WildcardUtil.Match(x, code)))
return false;
if (!config.WhitelistedContainersOnly)
return true;
HashSet<string> whitelist = ParseContainerCodes(config.WhitelistedContainers);
return whitelist.Any(x => WildcardUtil.Match(x, code));
}
private static HashSet<string> ParseContainerCodes(string rawList)
{
if (string.IsNullOrWhiteSpace(rawList))
return [];
string[] entries = list_separator_regex.Split(rawList.Trim());
HashSet<string> result = new(StringComparer.OrdinalIgnoreCase);
foreach (string entry in entries)
{
if (!string.IsNullOrWhiteSpace(entry))
result.Add(entry);
}
return result;
}
[GeneratedRegex("[,;\\s]+", RegexOptions.Compiled)]
private static partial Regex ListSeparatorRegex();
} }

File diff suppressed because it is too large Load Diff

View File

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