10 Commits
v1.1.0 ... main

15 changed files with 298 additions and 130 deletions

View File

@@ -1,5 +1,5 @@
{
"total": 73071,
"total": 90094,
"sessions": [
{
"begin": "2026-03-11T23:50:47+01:00",
@@ -70,6 +70,86 @@
"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",
"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": {
"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": []
},

View File

@@ -6,6 +6,8 @@
<OutputPath>bin\$(Configuration)\Mods\mod</OutputPath>
<Nullable>enable</Nullable>
<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>
<ConfigLibAvailable Condition="'$(VS_CONFIGLIB)' != '' and Exists('$(VS_CONFIGLIB)')">true</ConfigLibAvailable>
<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 bool HoldKey { get; set; } = true;
public bool BillboardIgnoreFront { get; set; } = false;
public int BillboardColumnsPerBlock { get; set; } = 4;

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.Rendering;
using System;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Config;
namespace ChestPreview.Core;
public class ChestPreviewModSystem : ModSystem
{
internal const string CONFIG_FILENAME = "chestpreview.json";
internal const string CONFIGLIB_DOMAIN = "chestpreview";
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();
private CardRenderer? cardRenderer;
private ConfigLibBridge? configLibBridge;
private StorageHoverHudRenderer? storageHoverHudRenderer;
private WorldBillboardRenderer? worldBillboardRenderer;
public override bool ShouldLoad(EnumAppSide forSide) => forSide is EnumAppSide.Client;
public override void StartClientSide(ICoreClientAPI api)
{
config = LoadClientConfig(api);
RegisterHotkeys(api);
if (api.ModLoader.IsModEnabled(ConfigLibBridge.CONFIGLIB_MODID))
configLibBridge = ConfigLibBridge.TryCreate(api, config);
cardRenderer = new CardRenderer(api, config);
storageHoverHudRenderer = new StorageHoverHudRenderer(api, config, cardRenderer);
api.Event.RegisterRenderer(storageHoverHudRenderer, EnumRenderStage.Ortho, "chestpreview-storage-hover-hud");
worldBillboardRenderer = new WorldBillboardRenderer(api, config, cardRenderer);
api.Event.RegisterRenderer(worldBillboardRenderer, EnumRenderStage.AfterOIT, "chestpreview-world-billboard-preview");
}
public override void Dispose()
{
configLibBridge?.Dispose();
configLibBridge = null;
if (storageHoverHudRenderer is { } renderer)
{
renderer.Api.Event.UnregisterRenderer(renderer, EnumRenderStage.Ortho);
renderer.Dispose();
storageHoverHudRenderer = null;
}
if (worldBillboardRenderer is { } billboardRenderer)
{
billboardRenderer.Api.Event.UnregisterRenderer(billboardRenderer, EnumRenderStage.AfterOIT);
billboardRenderer.Dispose();
worldBillboardRenderer = null;
}
cardRenderer?.Dispose();
cardRenderer = null;
}
public Config LoadClientConfig(ICoreClientAPI api)
{
Config config;
try
{
config = api.LoadModConfig<Config>(CONFIG_FILENAME) ?? new Config();
}
catch (Exception ex)
{
Mod.Logger.Error("Error while loading config.");
Mod.Logger.Error(ex);
config = new Config();
}
config.Mode = PreviewModes.Normalize(config.Mode);
if (config.BillboardColumnsPerBlock <= 0)
config.BillboardColumnsPerBlock = 5;
if (config.ColumnsUnderCursor <= 0)
config.ColumnsUnderCursor = 10;
config.PreviewNearbyRadius = config.PreviewNearbyRadius;
api.StoreModConfig(config, CONFIG_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);
}
}
using ChestPreview.Configs;
using ChestPreview.Rendering;
using System;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Config;
namespace ChestPreview.Core;
public class ChestPreviewModSystem : ModSystem
{
internal const string CONFIG_FILENAME = "chestpreview.json";
internal const string STATE_FILENAME = "chestpreview-state.json";
internal const string CONFIGLIB_DOMAIN = "chestpreview";
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 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 CardRenderer? cardRenderer;
private ConfigLibBridge? configLibBridge;
private StorageHoverHudRenderer? storageHoverHudRenderer;
private WorldBillboardRenderer? worldBillboardRenderer;
public State State { get; } = new();
public override bool ShouldLoad(EnumAppSide forSide) => forSide is EnumAppSide.Client;
public override void StartClientSide(ICoreClientAPI api)
{
config = LoadClientConfig(api);
RegisterHotkeys(api);
if (api.ModLoader.IsModEnabled(ConfigLibBridge.CONFIGLIB_MODID))
configLibBridge = ConfigLibBridge.TryCreate(api, config);
cardRenderer = new CardRenderer(api, config);
storageHoverHudRenderer = new StorageHoverHudRenderer(api, config, cardRenderer);
api.Event.RegisterRenderer(storageHoverHudRenderer, EnumRenderStage.Ortho, "chestpreview-storage-hover-hud");
worldBillboardRenderer = new WorldBillboardRenderer(api, config, cardRenderer);
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()
{
configLibBridge?.Dispose();
configLibBridge = null;
if (storageHoverHudRenderer is { } renderer)
{
renderer.Api.Event.UnregisterRenderer(renderer, EnumRenderStage.Ortho);
renderer.Dispose();
storageHoverHudRenderer = null;
}
if (worldBillboardRenderer is { } billboardRenderer)
{
billboardRenderer.Api.Event.UnregisterRenderer(billboardRenderer, EnumRenderStage.AfterOIT);
billboardRenderer.Dispose();
worldBillboardRenderer = null;
}
cardRenderer?.Dispose();
cardRenderer = null;
}
public Config LoadClientConfig(ICoreClientAPI api)
{
Config config;
State state;
try
{
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)
{
Mod.Logger.Error("Error while loading config.");
Mod.Logger.Error(ex);
config = new Config();
}
config.Mode = PreviewModes.Normalize(config.Mode);
if (config.BillboardColumnsPerBlock <= 0)
config.BillboardColumnsPerBlock = 5;
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

@@ -24,6 +24,7 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
private readonly Config config = config;
private readonly List<BlockEntity> nearbyContainerEntities = [];
private float nearbyScanAccumulator;
private readonly Lazy<State> state = new(() => api.ModLoader.GetModSystem<ChestPreviewModSystem>().State);
public void CollectTargets(float deltaTime, List<PreviewTarget> targets)
{
@@ -83,13 +84,16 @@ internal partial class PreviewTargetProvider(ICoreClientAPI api, Config config)
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;
if (config.HoldKey && !IsHotkeyHeld(ChestPreviewModSystem.PREVIEW_CONTAINERS_HOTKEY_CODE))
return PreviewModes.NONE;
if (state.Value.PreviewToggleActive || previewKeyHeld)
return PreviewModes.Normalize(config.Mode);
return PreviewModes.Normalize(config.Mode);
return PreviewModes.NONE;
}
private bool IsHotkeyHeld(string hotkeyCode)

View File

@@ -11,12 +11,13 @@ using Vintagestory.GameContent;
namespace ChestPreview.Rendering;
internal class WorldBillboardPresenter(ICoreClientAPI api)
internal class WorldBillboardPresenter(ICoreClientAPI api, Config config)
{
private const int multiblock_scan_radius = 2;
private static readonly float front_face_offset = 0.05f;
private readonly ICoreClientAPI api = api;
private readonly Config config = config;
private readonly List<BillboardTarget> frameBillboards = [];
public IReadOnlyList<BillboardTarget> FrameBillboards => frameBillboards;
@@ -162,7 +163,7 @@ internal class WorldBillboardPresenter(ICoreClientAPI api)
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));
@@ -171,7 +172,7 @@ internal class WorldBillboardPresenter(ICoreClientAPI api)
return true;
}
if (TryGetBlockSideFacing(block, out forward))
if (!config.BillboardIgnoreFront && TryGetBlockSideFacing(block, out forward))
{
BuildBasisFromForward(forward, out right, out up);
return true;

View File

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

View File

@@ -21,10 +21,10 @@
"clientSide": true
},
{
"code": "HoldKey",
"comment": "config-desc-HoldKey",
"code": "BillboardIgnoreFront",
"comment": "config-desc-BillboardIgnoreFront",
"type": "boolean",
"default": true,
"default": false,
"clientSide": true
},
{

View File

@@ -1,8 +1,10 @@
{
"hotkey-preview-container": "Preview container",
"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-HoldKey": "Previews only show while the \"Preview containers\" key is held.",
"config-desc-BillboardIgnoreFront": "With this set to false, in-world previews show on the front side of containers. By setting this to true, the previews show on the side determined to be most visible.",
"config-desc-BillboardColumnsPerBlock": "Columns per block width used for world billboards. For containers 2 blocks wide it's doubled.",
"config-desc-ColumnsUnderCursor": "Columns in the UnderCursor mode.",
"config-desc-PreviewNearbyRadius": "Radius for \"Preview containers nearby\".",

View File

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

View File

@@ -4,10 +4,15 @@
<TargetFramework>net8.0</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<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>
<ItemGroup>
<PackageReference Include="Cake.Frosting" Version="5.0.0" />

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
- `VINTAGE_STORY`: 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_21`: path to the 1.21 game install (on Windows default is %appdata%/VintageStory)
- `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_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