Compare commits
2 Commits
b29ea9faf3
...
eb34554a54
| Author | SHA1 | Date | |
|---|---|---|---|
|
eb34554a54
|
|||
|
288ce560f3
|
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
*.zip
|
||||
@@ -0,0 +1,13 @@
|
||||
name=Knox Buildworks - Viewpoint Compatibility
|
||||
id=hrsys_knoxbuildworksviewpoint
|
||||
poster=../common/media/textures/preview.png
|
||||
|
||||
description=3D military crate models for Knox Buildworks in Project Viewpoint.
|
||||
author=Riggs0
|
||||
category=building
|
||||
icon=../common/media/textures/preview.png
|
||||
url=https://hudsonriggs.systems
|
||||
modversion=2.1.0
|
||||
versionMin=42.20.0
|
||||
require=KnoxBuildworks,Viewpoint,PZVoxelStudioViewpoint
|
||||
loadAfter=KnoxBuildworks,PZVoxelStudioViewpoint,Viewpoint
|
||||
@@ -1,3 +1,51 @@
|
||||
# KnoxBuildWorksViewpoint
|
||||
|
||||
Adds Support using 6244 Models to Knoxbuild Works Buildables.
|
||||
Local compatibility patch using sour_kisel's 6244 models for Knox Buildworks in Project Viewpoint. Includes green and white military crates; gray military crates use the green meshes with a `#626763` gray-green retexture. Both orientations and stacked variants are included (18 sprite bindings).
|
||||
|
||||
## Mod layout
|
||||
|
||||
The repository root is the mod folder, ready to link directly into `Zomboid/mods`:
|
||||
|
||||
```text
|
||||
KnoxBuildWorksViewpoint/
|
||||
common/media/ Lua, model manifest, selected meshes/materials/textures
|
||||
42.20/mod.info Build 42.20+ version metadata
|
||||
mod.info Root metadata
|
||||
tools/ Matcher, reviewed rules and regression tests
|
||||
reports/ Catalogue coverage and remaining model gaps
|
||||
docs/ Detailed notes and mesh preview
|
||||
```
|
||||
|
||||
The mod ID is **hrsys_knoxbuildworksviewpoint**. Enable it after Knox Buildworks, Project Viewpoint and PZVoxelStudioViewpoint. Existing saves or server configurations using the earlier ID `KnoxBuildworksViewpointCrates` need to enable the new ID. Fully restart Zomboid after updates.
|
||||
|
||||
## Link the repository
|
||||
|
||||
After moving aside the previous copied folder at `Zomboid/mods/KnoxBuildworksViewpointCrates`, run in PowerShell with symbolic-link support:
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\Zomboid\mods\hrsys_knoxbuildworksviewpoint" -Target "$env:USERPROFILE\Git\KnoxBuildWorksViewpoint"
|
||||
```
|
||||
|
||||
The game then reads `common` and `42.20` directly from this checkout. Use one copy of this mod ID in the mods folder. The migration did not replace your existing local installation or create the link.
|
||||
|
||||
## Rebuild
|
||||
|
||||
From the repository root:
|
||||
|
||||
```powershell
|
||||
.\tools\Match-KnoxViewpoint.ps1
|
||||
```
|
||||
|
||||
Defaults rebuild the runtime files into this repository root, retain the `42.20` layout, and write audit data to `reports/`. Python 3.10+ and Pillow are required; the launcher defaults to this computer's bundled Codex Python. Pass `-PythonExecutable`, `-WorkshopRoot`, or `-GameDirectory` for other installations. Dependencies are listed in `tools/requirements.txt`.
|
||||
|
||||
`-Install` copies runtime files to the local mods directory; it never copies `.git`, tools or reports. With a mods symlink pointing to this repository, a normal rebuild is sufficient.
|
||||
|
||||
Run regression checks with the same Python interpreter:
|
||||
|
||||
```powershell
|
||||
& "$env:USERPROFILE\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe" .\tools\Test-KnoxViewpointMatcher.py
|
||||
```
|
||||
|
||||
Manual model choices and tint are stored in `tools/reviewed-matches.json`. [Remaining gaps](reports/unmatched.md), [full matching results](reports/results.md), and [mesh preview](docs/crate-model-preview.png) are included. Native wall/floor/roof rendering is distinguished from authored 6244 model matches; the patch does not claim to replace every buildable.
|
||||
|
||||
This is a private local compatibility build. Source models/textures belong to sour_kisel, and the original Workshop files are not modified. The copied model assets do not imply permission to redistribute them.
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,47 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4796500 0.4686500 2.4495000
|
||||
v 0.4796500 -0.4686500 2.4495000
|
||||
v 0.4796500 0.4686500 1.6700000
|
||||
v 0.4796500 -0.4686500 1.6700000
|
||||
v -0.4576500 0.4686500 1.6700000
|
||||
v -0.4576500 -0.4686500 1.6700000
|
||||
v -0.4576500 0.4686500 2.4495000
|
||||
v -0.4576500 -0.4686500 2.4495000
|
||||
vt 0.007813 0.968750
|
||||
vt 0.007813 0.031250
|
||||
vt 0.203125 0.968750
|
||||
vt 0.203125 0.031250
|
||||
vt 0.968750 0.968750
|
||||
vt 0.468750 0.968750
|
||||
vt 0.468750 0.187500
|
||||
vt 0.703125 0.968750
|
||||
vt 0.703125 0.187500
|
||||
vt 0.718750 0.968750
|
||||
vt 0.718750 0.187500
|
||||
vt 0.953125 0.968750
|
||||
vt 0.953125 0.187500
|
||||
vt 0.218750 0.968750
|
||||
vt 0.218750 0.031250
|
||||
vt 0.453125 0.968750
|
||||
vt 0.453125 0.031250
|
||||
vt 0.984375 0.968750
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/5/2 7/5/2
|
||||
f 6/5/2 8/5/2 7/5/2
|
||||
f 5/6/3 7/7/3 3/8/3
|
||||
f 7/7/3 1/9/3 3/8/3
|
||||
f 8/10/4 6/11/4 2/12/4
|
||||
f 6/11/4 4/13/4 2/12/4
|
||||
f 7/14/5 8/15/5 1/16/5
|
||||
f 8/15/5 2/17/5 1/16/5
|
||||
f 3/18/6 4/18/6 5/18/6
|
||||
f 4/18/6 6/18/6 5/18/6
|
||||
|
After Width: | Height: | Size: 1010 B |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 1010 B |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,47 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4686500 0.4796500 2.4495000
|
||||
v 0.4686500 -0.4576500 2.4495000
|
||||
v 0.4686500 0.4796500 1.6700000
|
||||
v 0.4686500 -0.4576500 1.6700000
|
||||
v -0.4686500 0.4796500 1.6700000
|
||||
v -0.4686500 -0.4576500 1.6700000
|
||||
v -0.4686500 0.4796500 2.4495000
|
||||
v -0.4686500 -0.4576500 2.4495000
|
||||
vt 0.007813 0.968750
|
||||
vt 0.007813 0.031250
|
||||
vt 0.203125 0.968750
|
||||
vt 0.203125 0.031250
|
||||
vt 0.218750 0.968750
|
||||
vt 0.218750 0.031250
|
||||
vt 0.414063 0.968750
|
||||
vt 0.414063 0.031250
|
||||
vt 0.679688 0.968750
|
||||
vt 0.679688 0.187500
|
||||
vt 0.914063 0.968750
|
||||
vt 0.914063 0.187500
|
||||
vt 0.929688 0.968750
|
||||
vt 0.429688 0.968750
|
||||
vt 0.429688 0.031250
|
||||
vt 0.664063 0.968750
|
||||
vt 0.664063 0.031250
|
||||
vt 0.945313 0.968750
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/13/4 2/13/4
|
||||
f 6/13/4 4/13/4 2/13/4
|
||||
f 7/14/5 8/15/5 1/16/5
|
||||
f 8/15/5 2/17/5 1/16/5
|
||||
f 3/18/6 4/18/6 5/18/6
|
||||
f 4/18/6 6/18/6 5/18/6
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,47 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4796500 0.4686500 2.4495000
|
||||
v 0.4796500 -0.4686500 2.4495000
|
||||
v 0.4796500 0.4686500 1.6700000
|
||||
v 0.4796500 -0.4686500 1.6700000
|
||||
v -0.4576500 0.4686500 1.6700000
|
||||
v -0.4576500 -0.4686500 1.6700000
|
||||
v -0.4576500 0.4686500 2.4495000
|
||||
v -0.4576500 -0.4686500 2.4495000
|
||||
vt 0.007813 0.968750
|
||||
vt 0.007813 0.031250
|
||||
vt 0.203125 0.968750
|
||||
vt 0.203125 0.031250
|
||||
vt 0.968750 0.968750
|
||||
vt 0.468750 0.968750
|
||||
vt 0.468750 0.187500
|
||||
vt 0.703125 0.968750
|
||||
vt 0.703125 0.187500
|
||||
vt 0.718750 0.968750
|
||||
vt 0.718750 0.187500
|
||||
vt 0.953125 0.968750
|
||||
vt 0.953125 0.187500
|
||||
vt 0.218750 0.968750
|
||||
vt 0.218750 0.031250
|
||||
vt 0.453125 0.968750
|
||||
vt 0.453125 0.031250
|
||||
vt 0.984375 0.968750
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/5/2 7/5/2
|
||||
f 6/5/2 8/5/2 7/5/2
|
||||
f 5/6/3 7/7/3 3/8/3
|
||||
f 7/7/3 1/9/3 3/8/3
|
||||
f 8/10/4 6/11/4 2/12/4
|
||||
f 6/11/4 4/13/4 2/12/4
|
||||
f 7/14/5 8/15/5 1/16/5
|
||||
f 8/15/5 2/17/5 1/16/5
|
||||
f 3/18/6 4/18/6 5/18/6
|
||||
f 4/18/6 6/18/6 5/18/6
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,47 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4686500 0.4796500 2.4495000
|
||||
v 0.4686500 -0.4576500 2.4495000
|
||||
v 0.4686500 0.4796500 1.6700000
|
||||
v 0.4686500 -0.4576500 1.6700000
|
||||
v -0.4686500 0.4796500 1.6700000
|
||||
v -0.4686500 -0.4576500 1.6700000
|
||||
v -0.4686500 0.4796500 2.4495000
|
||||
v -0.4686500 -0.4576500 2.4495000
|
||||
vt 0.007813 0.968750
|
||||
vt 0.007813 0.031250
|
||||
vt 0.203125 0.968750
|
||||
vt 0.203125 0.031250
|
||||
vt 0.218750 0.968750
|
||||
vt 0.218750 0.031250
|
||||
vt 0.414063 0.968750
|
||||
vt 0.414063 0.031250
|
||||
vt 0.679688 0.968750
|
||||
vt 0.679688 0.187500
|
||||
vt 0.914063 0.968750
|
||||
vt 0.914063 0.187500
|
||||
vt 0.929688 0.968750
|
||||
vt 0.429688 0.968750
|
||||
vt 0.429688 0.031250
|
||||
vt 0.664063 0.968750
|
||||
vt 0.664063 0.031250
|
||||
vt 0.945313 0.968750
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/13/4 2/13/4
|
||||
f 6/13/4 4/13/4 2/13/4
|
||||
f 7/14/5 8/15/5 1/16/5
|
||||
f 8/15/5 2/17/5 1/16/5
|
||||
f 3/18/6 4/18/6 5/18/6
|
||||
f 4/18/6 6/18/6 5/18/6
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,47 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4686500 0.4796500 2.4495000
|
||||
v 0.4686500 -0.4576500 2.4495000
|
||||
v 0.4686500 0.4796500 1.6700000
|
||||
v 0.4686500 -0.4576500 1.6700000
|
||||
v -0.4686500 0.4796500 1.6700000
|
||||
v -0.4686500 -0.4576500 1.6700000
|
||||
v -0.4686500 0.4796500 2.4495000
|
||||
v -0.4686500 -0.4576500 2.4495000
|
||||
vt 0.007813 0.968750
|
||||
vt 0.007813 0.031250
|
||||
vt 0.203125 0.968750
|
||||
vt 0.203125 0.031250
|
||||
vt 0.218750 0.968750
|
||||
vt 0.218750 0.031250
|
||||
vt 0.414063 0.968750
|
||||
vt 0.414063 0.031250
|
||||
vt 0.679688 0.968750
|
||||
vt 0.679688 0.187500
|
||||
vt 0.914063 0.968750
|
||||
vt 0.914063 0.187500
|
||||
vt 0.929688 0.968750
|
||||
vt 0.429688 0.968750
|
||||
vt 0.429688 0.031250
|
||||
vt 0.664063 0.968750
|
||||
vt 0.664063 0.031250
|
||||
vt 0.945313 0.968750
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/13/4 2/13/4
|
||||
f 6/13/4 4/13/4 2/13/4
|
||||
f 7/14/5 8/15/5 1/16/5
|
||||
f 8/15/5 2/17/5 1/16/5
|
||||
f 3/18/6 4/18/6 5/18/6
|
||||
f 4/18/6 6/18/6 5/18/6
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,53 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4003000 0.3968000 0.8535150
|
||||
v 0.4003000 -0.4568000 0.8535150
|
||||
v 0.4003000 0.3968000 0.0000050
|
||||
v 0.4003000 -0.4568000 0.0000050
|
||||
v -0.4533000 0.3968000 0.0000050
|
||||
v -0.4533000 -0.4568000 0.0000050
|
||||
v -0.4533000 0.3968000 0.8535150
|
||||
v -0.4533000 -0.4568000 0.8535150
|
||||
vt 0.007813 0.983333
|
||||
vt 0.007813 0.525000
|
||||
vt 0.222656 0.983333
|
||||
vt 0.222656 0.525000
|
||||
vt 0.238281 0.983333
|
||||
vt 0.238281 0.525000
|
||||
vt 0.453125 0.983333
|
||||
vt 0.453125 0.525000
|
||||
vt 0.468750 0.983333
|
||||
vt 0.468750 0.525000
|
||||
vt 0.683594 0.983333
|
||||
vt 0.683594 0.525000
|
||||
vt 0.699219 0.983333
|
||||
vt 0.699219 0.525000
|
||||
vt 0.914063 0.983333
|
||||
vt 0.914063 0.525000
|
||||
vt 0.007813 0.491667
|
||||
vt 0.007813 0.033333
|
||||
vt 0.222656 0.491667
|
||||
vt 0.222656 0.033333
|
||||
vt 0.238281 0.491667
|
||||
vt 0.238281 0.033333
|
||||
vt 0.453125 0.491667
|
||||
vt 0.453125 0.033333
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/14/4 2/15/4
|
||||
f 6/14/4 4/16/4 2/15/4
|
||||
f 7/17/5 8/18/5 1/19/5
|
||||
f 8/18/5 2/20/5 1/19/5
|
||||
f 3/21/6 4/22/6 5/23/6
|
||||
f 4/22/6 6/24/6 5/23/6
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,50 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4184400 0.4184400 1.7200000
|
||||
v 0.4184400 -0.4184400 1.7200000
|
||||
v 0.4184400 0.4184400 0.8500000
|
||||
v 0.4184400 -0.4184400 0.8500000
|
||||
v -0.4184400 0.4184400 0.8500000
|
||||
v -0.4184400 -0.4184400 0.8500000
|
||||
v -0.4184400 0.4184400 1.7200000
|
||||
v -0.4184400 -0.4184400 1.7200000
|
||||
vt 0.460938 0.983333
|
||||
vt 0.460938 0.533333
|
||||
vt 0.679688 0.983333
|
||||
vt 0.679688 0.533333
|
||||
vt 0.695313 0.983333
|
||||
vt 0.695313 0.533333
|
||||
vt 0.914063 0.983333
|
||||
vt 0.914063 0.533333
|
||||
vt 0.007813 0.983333
|
||||
vt 0.007813 0.516667
|
||||
vt 0.218750 0.983333
|
||||
vt 0.218750 0.516667
|
||||
vt 0.234375 0.983333
|
||||
vt 0.234375 0.516667
|
||||
vt 0.445313 0.983333
|
||||
vt 0.445313 0.516667
|
||||
vt 0.007813 0.483333
|
||||
vt 0.007813 0.033333
|
||||
vt 0.218750 0.483333
|
||||
vt 0.218750 0.033333
|
||||
vt 0.234375 0.483333
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/14/4 2/15/4
|
||||
f 6/14/4 4/16/4 2/15/4
|
||||
f 7/17/5 8/18/5 1/19/5
|
||||
f 8/18/5 2/20/5 1/19/5
|
||||
f 3/21/6 4/21/6 5/21/6
|
||||
f 4/21/6 6/21/6 5/21/6
|
||||
|
After Width: | Height: | Size: 9.8 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,50 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4184400 0.4184400 1.7200000
|
||||
v 0.4184400 -0.4184400 1.7200000
|
||||
v 0.4184400 0.4184400 0.8500000
|
||||
v 0.4184400 -0.4184400 0.8500000
|
||||
v -0.4184400 0.4184400 0.8500000
|
||||
v -0.4184400 -0.4184400 0.8500000
|
||||
v -0.4184400 0.4184400 1.7200000
|
||||
v -0.4184400 -0.4184400 1.7200000
|
||||
vt 0.460938 0.983333
|
||||
vt 0.460938 0.533333
|
||||
vt 0.679688 0.983333
|
||||
vt 0.679688 0.533333
|
||||
vt 0.695313 0.983333
|
||||
vt 0.695313 0.533333
|
||||
vt 0.914063 0.983333
|
||||
vt 0.914063 0.533333
|
||||
vt 0.007813 0.983333
|
||||
vt 0.007813 0.516667
|
||||
vt 0.218750 0.983333
|
||||
vt 0.218750 0.516667
|
||||
vt 0.234375 0.983333
|
||||
vt 0.234375 0.516667
|
||||
vt 0.445313 0.983333
|
||||
vt 0.445313 0.516667
|
||||
vt 0.007813 0.483333
|
||||
vt 0.007813 0.033333
|
||||
vt 0.218750 0.483333
|
||||
vt 0.218750 0.033333
|
||||
vt 0.234375 0.483333
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/14/4 2/15/4
|
||||
f 6/14/4 4/16/4 2/15/4
|
||||
f 7/17/5 8/18/5 1/19/5
|
||||
f 8/18/5 2/20/5 1/19/5
|
||||
f 3/21/6 4/21/6 5/21/6
|
||||
f 4/21/6 6/21/6 5/21/6
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,53 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.3877800 0.3932800 0.8535150
|
||||
v 0.3877800 -0.4532800 0.8535150
|
||||
v 0.3877800 0.3932800 0.0000050
|
||||
v 0.3877800 -0.4532800 0.0000050
|
||||
v -0.4587800 0.3932800 0.0000050
|
||||
v -0.4587800 -0.4532800 0.0000050
|
||||
v -0.4587800 0.3932800 0.8535150
|
||||
v -0.4587800 -0.4532800 0.8535150
|
||||
vt 0.007813 0.983333
|
||||
vt 0.007813 0.525000
|
||||
vt 0.222656 0.983333
|
||||
vt 0.222656 0.525000
|
||||
vt 0.238281 0.983333
|
||||
vt 0.238281 0.525000
|
||||
vt 0.453125 0.983333
|
||||
vt 0.453125 0.525000
|
||||
vt 0.468750 0.983333
|
||||
vt 0.468750 0.525000
|
||||
vt 0.683594 0.983333
|
||||
vt 0.683594 0.525000
|
||||
vt 0.699219 0.983333
|
||||
vt 0.699219 0.525000
|
||||
vt 0.914063 0.983333
|
||||
vt 0.914063 0.525000
|
||||
vt 0.007813 0.491667
|
||||
vt 0.007813 0.033333
|
||||
vt 0.222656 0.491667
|
||||
vt 0.222656 0.033333
|
||||
vt 0.238281 0.491667
|
||||
vt 0.238281 0.033333
|
||||
vt 0.453125 0.491667
|
||||
vt 0.453125 0.033333
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/6/2 7/7/2
|
||||
f 6/6/2 8/8/2 7/7/2
|
||||
f 5/9/3 7/10/3 3/11/3
|
||||
f 7/10/3 1/12/3 3/11/3
|
||||
f 8/13/4 6/14/4 2/15/4
|
||||
f 6/14/4 4/16/4 2/15/4
|
||||
f 7/17/5 8/18/5 1/19/5
|
||||
f 8/18/5 2/20/5 1/19/5
|
||||
f 3/21/6 4/22/6 5/23/6
|
||||
f 4/22/6 6/24/6 5/23/6
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,4 @@
|
||||
newmtl atlas
|
||||
Kd 1 1 1
|
||||
d 1
|
||||
map_Kd model-rp-texture.png
|
||||
@@ -0,0 +1,47 @@
|
||||
# PZ Voxel Studio — Z up, tile units
|
||||
mtllib model-rp.mtl
|
||||
usemtl atlas
|
||||
v 0.4796500 0.4686500 2.4495000
|
||||
v 0.4796500 -0.4686500 2.4495000
|
||||
v 0.4796500 0.4686500 1.6700000
|
||||
v 0.4796500 -0.4686500 1.6700000
|
||||
v -0.4576500 0.4686500 1.6700000
|
||||
v -0.4576500 -0.4686500 1.6700000
|
||||
v -0.4576500 0.4686500 2.4495000
|
||||
v -0.4576500 -0.4686500 2.4495000
|
||||
vt 0.007813 0.968750
|
||||
vt 0.007813 0.031250
|
||||
vt 0.203125 0.968750
|
||||
vt 0.203125 0.031250
|
||||
vt 0.968750 0.968750
|
||||
vt 0.468750 0.968750
|
||||
vt 0.468750 0.187500
|
||||
vt 0.703125 0.968750
|
||||
vt 0.703125 0.187500
|
||||
vt 0.718750 0.968750
|
||||
vt 0.718750 0.187500
|
||||
vt 0.953125 0.968750
|
||||
vt 0.953125 0.187500
|
||||
vt 0.218750 0.968750
|
||||
vt 0.218750 0.031250
|
||||
vt 0.453125 0.968750
|
||||
vt 0.453125 0.031250
|
||||
vt 0.984375 0.968750
|
||||
vn 1.0000000 0.0000000 0.0000000
|
||||
vn -1.0000000 0.0000000 0.0000000
|
||||
vn 0.0000000 1.0000000 0.0000000
|
||||
vn 0.0000000 -1.0000000 0.0000000
|
||||
vn 0.0000000 0.0000000 1.0000000
|
||||
vn 0.0000000 0.0000000 -1.0000000
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 2/2/1 4/4/1 3/3/1
|
||||
f 5/5/2 6/5/2 7/5/2
|
||||
f 6/5/2 8/5/2 7/5/2
|
||||
f 5/6/3 7/7/3 3/8/3
|
||||
f 7/7/3 1/9/3 3/8/3
|
||||
f 8/10/4 6/11/4 2/12/4
|
||||
f 6/11/4 4/13/4 2/12/4
|
||||
f 7/14/5 8/15/5 1/16/5
|
||||
f 8/15/5 2/17/5 1/16/5
|
||||
f 3/18/6 4/18/6 5/18/6
|
||||
f 4/18/6 6/18/6 5/18/6
|
||||
@@ -0,0 +1,39 @@
|
||||
id=knox-buildworks-viewpoint-crates
|
||||
name=Knox Buildworks - Viewpoint Compatibility
|
||||
version=2
|
||||
model.kbw-72ebafe55892a582=knox-crates-assets/72ebafe55892a582/model.obj
|
||||
bind.kbw_location_military_generic_01_0=kbw-72ebafe55892a582,0,0.04,0.04,0,1
|
||||
model.kbw-d91d0882b73a2252=knox-crates-assets/d91d0882b73a2252/model.obj
|
||||
bind.kbw_location_military_generic_01_1=kbw-d91d0882b73a2252,0,0.04,0.04,0,1
|
||||
model.kbw-tint-626763-6a5f44c9aabb01ee=knox-crates-assets/tint-626763-6a5f44c9aabb01ee/model.obj
|
||||
bind.kbw_location_military_generic_01_10=kbw-tint-626763-6a5f44c9aabb01ee,0,0.04,0.04,0,1
|
||||
model.kbw-tint-626763-bf6ea4d5d6468188=knox-crates-assets/tint-626763-bf6ea4d5d6468188/model.obj
|
||||
bind.kbw_location_military_generic_01_11=kbw-tint-626763-bf6ea4d5d6468188,0,0.04,0.04,0,1
|
||||
model.kbw-tint-626763-9776d89886e00c7f=knox-crates-assets/tint-626763-9776d89886e00c7f/model.obj
|
||||
bind.kbw_location_military_generic_01_12=kbw-tint-626763-9776d89886e00c7f,0,-0.035,-0.025,0,1
|
||||
model.kbw-tint-626763-f56963fe3c37b654=knox-crates-assets/tint-626763-f56963fe3c37b654/model.obj
|
||||
bind.kbw_location_military_generic_01_13=kbw-tint-626763-f56963fe3c37b654,0,-0.025,-0.035,0,1
|
||||
model.kbw-white-f3df912eeaed430c=knox-crates-assets/white-f3df912eeaed430c/model.obj
|
||||
bind.kbw_location_military_generic_01_16=kbw-white-f3df912eeaed430c,0,0.04,0.04,0,1
|
||||
model.kbw-white-928bbed08a41810b=knox-crates-assets/white-928bbed08a41810b/model.obj
|
||||
bind.kbw_location_military_generic_01_17=kbw-white-928bbed08a41810b,0,0.04,0.04,0,1
|
||||
model.kbw-white-de21ea154ef233a5=knox-crates-assets/white-de21ea154ef233a5/model.obj
|
||||
bind.kbw_location_military_generic_01_18=kbw-white-de21ea154ef233a5,0,0.04,0.04,0,1
|
||||
model.kbw-white-9e753dc1ada886c6=knox-crates-assets/white-9e753dc1ada886c6/model.obj
|
||||
bind.kbw_location_military_generic_01_19=kbw-white-9e753dc1ada886c6,0,0.04,0.04,0,1
|
||||
model.kbw-6a5f44c9aabb01ee=knox-crates-assets/6a5f44c9aabb01ee/model.obj
|
||||
bind.kbw_location_military_generic_01_2=kbw-6a5f44c9aabb01ee,0,0.04,0.04,0,1
|
||||
model.kbw-white-f70e1705d8050fed=knox-crates-assets/white-f70e1705d8050fed/model.obj
|
||||
bind.kbw_location_military_generic_01_20=kbw-white-f70e1705d8050fed,0,-0.035,-0.025,0,1
|
||||
model.kbw-white-21799e9c7e36ee0a=knox-crates-assets/white-21799e9c7e36ee0a/model.obj
|
||||
bind.kbw_location_military_generic_01_21=kbw-white-21799e9c7e36ee0a,0,-0.025,-0.035,0,1
|
||||
model.kbw-bf6ea4d5d6468188=knox-crates-assets/bf6ea4d5d6468188/model.obj
|
||||
bind.kbw_location_military_generic_01_3=kbw-bf6ea4d5d6468188,0,0.04,0.04,0,1
|
||||
model.kbw-9776d89886e00c7f=knox-crates-assets/9776d89886e00c7f/model.obj
|
||||
bind.kbw_location_military_generic_01_4=kbw-9776d89886e00c7f,0,-0.035,-0.025,0,1
|
||||
model.kbw-f56963fe3c37b654=knox-crates-assets/f56963fe3c37b654/model.obj
|
||||
bind.kbw_location_military_generic_01_5=kbw-f56963fe3c37b654,0,-0.025,-0.035,0,1
|
||||
model.kbw-tint-626763-72ebafe55892a582=knox-crates-assets/tint-626763-72ebafe55892a582/model.obj
|
||||
bind.kbw_location_military_generic_01_8=kbw-tint-626763-72ebafe55892a582,0,0.04,0.04,0,1
|
||||
model.kbw-tint-626763-d91d0882b73a2252=knox-crates-assets/tint-626763-d91d0882b73a2252/model.obj
|
||||
bind.kbw_location_military_generic_01_9=kbw-tint-626763-d91d0882b73a2252,0,0.04,0.04,0,1
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Render bindings only. No recipe, object, inventory or save changes.
|
||||
local registered = false
|
||||
local warned = false
|
||||
local function register()
|
||||
if registered or not Viewpoint or not Viewpoint.ModelPacks then return end
|
||||
local ok, result = pcall(Viewpoint.ModelPacks.register,
|
||||
"hrsys_knoxbuildworksviewpoint", "media/knox-viewpoint-crates.properties")
|
||||
if ok and result then
|
||||
registered = true
|
||||
print("[Knox Viewpoint Crates] Registered 18 reviewed compatibility sprite bindings.")
|
||||
elseif not warned then
|
||||
warned = true
|
||||
print("[Knox Viewpoint Crates] Model pack registration failed: " .. tostring(result))
|
||||
end
|
||||
end
|
||||
Events.OnGameBoot.Add(register)
|
||||
Events.OnGameStart.Add(register)
|
||||
register()
|
||||
|
After Width: | Height: | Size: 123 KiB |
@@ -0,0 +1,54 @@
|
||||
# Knox Buildworks - Viewpoint Compatibility
|
||||
|
||||
Enable **Knox Buildworks - Viewpoint Compatibility** after Knox Buildworks, Project Viewpoint and 6244 models for Viewpoint. The mod ID is `hrsys_knoxbuildworksviewpoint`. Existing saves or server configurations using the earlier ID `KnoxBuildworksViewpointCrates` need to enable the new ID. Fully restart the game after this update.
|
||||
|
||||
Suggested mods link: `C:\Users\Hudson\Zomboid\mods\hrsys_knoxbuildworksviewpoint`, pointing at the repository root.
|
||||
|
||||
## What it actually matches
|
||||
|
||||
- Green military crate: both orientations and stacked sprite variants, six bindings.
|
||||
- Gray military crate: the green crate meshes in both orientations and stacked variants, retextured toward **#626763** with the source shading/detail preserved; six bindings. Lighting changes its apparent color in-game.
|
||||
- White military crate: six manually adapted variants. These reuse the gray crate meshes and UVs with a neutral white texture atlas. White color is an approximation. The object's custom paint tint is not added by this model pack.
|
||||
|
||||
The matching script can identify direct authored bindings, verify Knox sprite aliases by object/container metadata, and find identical sprite artwork with the same anchor, family and unambiguous model transform. It ranks less certain candidates for review. It does not automatically apply a model just because its name or tile number is similar.
|
||||
|
||||
This distinction matters: Knox white crate indices 16/17 correspond to military **signs** in vanilla. That automatic alias is rejected; the reviewed rules use actual crate meshes instead.
|
||||
|
||||
## Catalogue results
|
||||
|
||||
The expanded scan checks all **3,467 catalogue entries**, including templates, material/variant sprites, finishes, door states and game-defined entity SpriteConfig rows. The previous simple scan omitted some inherited/game-defined references. Current results supersede the older `buildworks-model-audit.json` report.
|
||||
|
||||
The installed authored model pack already binds 2,821 distinct sprite names referenced by this catalogue. This compatibility patch adds 18 bindings. Most other references have native floor, edge, tile geometry, overlay or roof metadata. Those are recorded separately: lacking a 6244 model binding does not mean the object is invisible. Roof-family classifications still require visual checking.
|
||||
|
||||
**216 buildables have parts with no accepted compatible authored model (561 sprite references).** I reviewed their families, metadata, candidate matches and relevant sprite contact sheets. They include specialized wall/door trims, curtains/open states, particular window shapes, signage and multi-tile railing parts. Assigning similar furniture, signs with different artwork, or a complete model to each small section would produce incorrect objects. Their existing native rendering is retained. Faithful additional replacements require suitable model assets or new mesh/texture work; the script does not claim that these gaps are fixed.
|
||||
|
||||
- `reports/unmatched.md`: the focused list of remaining buildables and reasons.
|
||||
- `reports/results.md`: complete readable matching/native-rendering results.
|
||||
- `reports/applied-matches.json`: every applied source-to-target assignment.
|
||||
- `reports/sprite-decisions.json`: evidence, native routes and candidate shortlists per sprite.
|
||||
- `reports/buildable-coverage.json`: all catalogue entries with their referenced sprites/statuses.
|
||||
- `tools/reviewed-matches.json`: editable manual assignments, white adaptation rules, stack variants and review decisions.
|
||||
|
||||
## Run the matcher
|
||||
|
||||
From the repository root in PowerShell:
|
||||
|
||||
```powershell
|
||||
.\tools\Match-KnoxViewpoint.ps1
|
||||
```
|
||||
|
||||
The launcher uses the Python bundled with Codex on this computer. Elsewhere, pass `-PythonExecutable` with Python 3.10+ and Pillow installed. Pass `-WorkshopRoot` and `-GameDirectory` if Steam uses a different library. The default output is the repository root (`common` and `42.20`); reports go in `reports/`. Use a normal rebuild when the repository is symlinked into the mods folder.
|
||||
|
||||
`tools/Build-KnoxViewpointPatch.ps1` is now a compatibility launcher for the expanded matcher. Run the matcher again after Workshop updates. It reads the source author's actual manifest registration order, checks that reviewed crate targets/sources still identify crates, and copies only the selected OBJ files and their material/texture dependencies. Source Workshop files are untouched.
|
||||
|
||||
Add a reviewed assignment under `matches` in `tools/reviewed-matches.json` using an existing source sprite, evidence, and expected source/target CustomName. A regular assignment reuses the source model and transform; `adaptation: white-crate` additionally creates a white texture atlas. A `tint` value such as `#626763` retints a green atlas while preserving relative luminance and alpha. Reviewed choices take precedence over automatic aliases. Candidate shortlists are suggestions, not approved assignments. Rebuild after editing.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
- Matcher regression tests cover the crate/sign numbering collision, real aliases, properties-only tile blocks, variant IDs, capitalized/hyphenated sprite names, native routes, manifest precedence, review suggestions and white adaptation/source preservation.
|
||||
- Installed Viewpoint Java parsers accept all 18 bindings, meshes, materials and texture references with zero manifest errors.
|
||||
- Zomboid's Kahlua compiler and mocked boot/start registration/retry checks pass.
|
||||
- `docs/crate-model-preview.png` is an offline mesh preview, not an in-game screenshot.
|
||||
- In-game appearance has not been visually tested. Check green, gray and white crates in both orientations and in stacks after restarting. Existing crates should pick up their bindings when loaded. Colored paint finishes may remain neutral white in Viewpoint.
|
||||
|
||||
This patch changes rendering bindings only. It does not rewrite save objects, sprite names, recipes, inventory or container contents/capacity. Disable this patch to remove its bindings while keeping the original construction mods enabled. The bundled asset copies are for this private local build; sour_kisel owns the source models and textures.
|
||||
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"catalogueEntries": 3467,
|
||||
"entriesWithResolvedSprites": 3467,
|
||||
"uniqueSprites": 10836,
|
||||
"authoredPackBindings": 10166,
|
||||
"patchBindings": 18,
|
||||
"copiedModels": 18,
|
||||
"statusCounts": {
|
||||
"already-bound": 2821,
|
||||
"native-floor": 177,
|
||||
"native-floor-overlay": 482,
|
||||
"native-geometry": 196,
|
||||
"native-edge": 4882,
|
||||
"no-compatible-authored-model": 561,
|
||||
"automatic-match": 2,
|
||||
"manual-adaptation": 4,
|
||||
"native-roof-candidate": 1699,
|
||||
"reviewed-stack": 12
|
||||
},
|
||||
"note": "Native routes describe installed geometry/metadata, not verified in-game appearance. Roof family matches require visual checks. No-compatible-model entries have no accepted model assignment.",
|
||||
"unresolvedEntityReferences": {},
|
||||
"buildablesWithReviewedModelGaps": 216,
|
||||
"unreviewedGapSprites": 0
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
name=Knox Buildworks - Viewpoint Compatibility
|
||||
id=hrsys_knoxbuildworksviewpoint
|
||||
poster=common/media/textures/preview.png
|
||||
|
||||
description=3D military crate models for Knox Buildworks in Project Viewpoint.
|
||||
author=Riggs0
|
||||
category=building
|
||||
versionMin=42.20.0
|
||||
url=https://hudsonriggs.systems
|
||||
modversion=2.1.0
|
||||
icon=common/media/textures/preview.png
|
||||
require=KnoxBuildworks,Viewpoint,PZVoxelStudioViewpoint
|
||||
loadAfter=KnoxBuildworks,PZVoxelStudioViewpoint,Viewpoint
|
||||
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"kbw_location_military_generic_01_0": {
|
||||
"source": "location_military_generic_01_0",
|
||||
"method": "automatic-alias"
|
||||
},
|
||||
"kbw_location_military_generic_01_1": {
|
||||
"source": "location_military_generic_01_1",
|
||||
"method": "automatic-alias"
|
||||
},
|
||||
"kbw_location_military_generic_01_16": {
|
||||
"source": "location_military_generic_01_8",
|
||||
"method": "manual-adaptation",
|
||||
"adaptation": "white-crate",
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_17": {
|
||||
"source": "location_military_generic_01_9",
|
||||
"method": "manual-adaptation",
|
||||
"adaptation": "white-crate",
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_8": {
|
||||
"source": "location_military_generic_01_0",
|
||||
"method": "manual-adaptation",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763"
|
||||
},
|
||||
"kbw_location_military_generic_01_9": {
|
||||
"source": "location_military_generic_01_1",
|
||||
"method": "manual-adaptation",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763"
|
||||
},
|
||||
"kbw_location_military_generic_01_2": {
|
||||
"source": "location_military_generic_01_2",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": null,
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_3": {
|
||||
"source": "location_military_generic_01_3",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": null,
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_4": {
|
||||
"source": "location_military_generic_01_4",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": null,
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_5": {
|
||||
"source": "location_military_generic_01_5",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": null,
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_10": {
|
||||
"source": "location_military_generic_01_2",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763"
|
||||
},
|
||||
"kbw_location_military_generic_01_11": {
|
||||
"source": "location_military_generic_01_3",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763"
|
||||
},
|
||||
"kbw_location_military_generic_01_12": {
|
||||
"source": "location_military_generic_01_4",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763"
|
||||
},
|
||||
"kbw_location_military_generic_01_13": {
|
||||
"source": "location_military_generic_01_5",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763"
|
||||
},
|
||||
"kbw_location_military_generic_01_18": {
|
||||
"source": "location_military_generic_01_10",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "white-crate",
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_19": {
|
||||
"source": "location_military_generic_01_11",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "white-crate",
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_20": {
|
||||
"source": "location_military_generic_01_12",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "white-crate",
|
||||
"tint": null
|
||||
},
|
||||
"kbw_location_military_generic_01_21": {
|
||||
"source": "location_military_generic_01_13",
|
||||
"method": "reviewed-stack",
|
||||
"adaptation": "white-crate",
|
||||
"tint": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"catalogueEntries": 3467,
|
||||
"entriesWithResolvedSprites": 3467,
|
||||
"uniqueSprites": 10836,
|
||||
"authoredPackBindings": 10166,
|
||||
"patchBindings": 18,
|
||||
"copiedModels": 18,
|
||||
"statusCounts": {
|
||||
"already-bound": 2821,
|
||||
"native-floor": 177,
|
||||
"native-floor-overlay": 482,
|
||||
"native-geometry": 196,
|
||||
"native-edge": 4882,
|
||||
"no-compatible-authored-model": 561,
|
||||
"automatic-match": 2,
|
||||
"manual-adaptation": 4,
|
||||
"native-roof-candidate": 1699,
|
||||
"reviewed-stack": 12
|
||||
},
|
||||
"note": "Native routes describe installed geometry/metadata, not verified in-game appearance. Roof family matches require visual checks. No-compatible-model entries have no accepted model assignment.",
|
||||
"unresolvedEntityReferences": {},
|
||||
"buildablesWithReviewedModelGaps": 216,
|
||||
"unreviewedGapSprites": 0
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$WorkshopRoot = 'D:\SteamLibrary\steamapps\workshop\content\108600',
|
||||
[string]$GameDirectory = 'D:\SteamLibrary\steamapps\common\ProjectZomboid',
|
||||
[string]$OutputDirectory = (Split-Path -Parent $PSScriptRoot),
|
||||
[switch]$Install,
|
||||
[string]$LocalModsDirectory = (Join-Path $env:USERPROFILE 'Zomboid\mods'),
|
||||
[string]$PythonExecutable = (Join-Path $env:USERPROFILE '.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe')
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (!(Test-Path -LiteralPath $PythonExecutable)) {
|
||||
throw 'Pass -PythonExecutable with a Python 3.10+ interpreter containing Pillow.'
|
||||
}
|
||||
$scriptArguments = @((Join-Path $PSScriptRoot 'Match-KnoxViewpoint.py'), '--workshop-root', $WorkshopRoot,
|
||||
'--game-directory', $GameDirectory, '--output-directory', $OutputDirectory, '--local-mods-directory', $LocalModsDirectory)
|
||||
if ($Install) { $scriptArguments += '--install' }
|
||||
& $PythonExecutable @scriptArguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "Matcher failed with exit code $LASTEXITCODE" }
|
||||
@@ -0,0 +1,18 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$WorkshopRoot = 'D:\SteamLibrary\steamapps\workshop\content\108600',
|
||||
[string]$GameDirectory = 'D:\SteamLibrary\steamapps\common\ProjectZomboid',
|
||||
[string]$OutputDirectory = (Split-Path -Parent $PSScriptRoot),
|
||||
[switch]$Install,
|
||||
[string]$LocalModsDirectory = (Join-Path $env:USERPROFILE 'Zomboid\mods'),
|
||||
[string]$PythonExecutable = (Join-Path $env:USERPROFILE '.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe')
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (!(Test-Path -LiteralPath $PythonExecutable)) {
|
||||
throw 'Pass -PythonExecutable with a Python 3.10+ interpreter containing Pillow.'
|
||||
}
|
||||
$scriptArguments = @((Join-Path $PSScriptRoot 'Match-KnoxViewpoint.py'), '--workshop-root', $WorkshopRoot,
|
||||
'--game-directory', $GameDirectory, '--output-directory', $OutputDirectory, '--local-mods-directory', $LocalModsDirectory)
|
||||
if ($Install) { $scriptArguments += '--install' }
|
||||
& $PythonExecutable @scriptArguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "Matcher failed with exit code $LASTEXITCODE" }
|
||||
@@ -0,0 +1,601 @@
|
||||
"""Build a private local Viewpoint compatibility pack and an evidence-based catalogue audit.
|
||||
|
||||
Python 3.10+ and Pillow. Never changes Workshop files, saves, recipes or sprite IDs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import collections
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
MOD_ID = 'hrsys_knoxbuildworksviewpoint'
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO_ROOT = HERE.parent
|
||||
VERSION_FOLDER = '42.20'
|
||||
SPRITE = re.compile(r'^[A-Za-z][A-Za-z0-9_-]*_\d+$')
|
||||
SEMANTICS = ('CustomName', 'GroupName', 'MoveType', 'container', 'WindowShape')
|
||||
SHAPES = re.compile(r'\b(box|polygon|cylinder)\s*\{')
|
||||
|
||||
def read_json(path):
|
||||
return json.loads(Path(path).read_text(encoding='utf-8-sig'))
|
||||
|
||||
def write_json(path, value):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, ensure_ascii=False)+'\n', encoding='utf-8')
|
||||
|
||||
def family(sprite):
|
||||
return sprite.removeprefix('kbw_').rsplit('_', 1)[0]
|
||||
|
||||
def merge(parent, child):
|
||||
if isinstance(parent, dict) and isinstance(child, dict):
|
||||
out = copy.deepcopy(parent)
|
||||
for key, value in child.items():
|
||||
out[key] = merge(out.get(key), value)
|
||||
return out
|
||||
return copy.deepcopy(child)
|
||||
|
||||
def tile_properties(path):
|
||||
"""Read the installed game's tdef v1 format, including custom tiles and patches."""
|
||||
out = {}
|
||||
with path.open('rb') as stream:
|
||||
def integer():
|
||||
return struct.unpack('<i', stream.read(4))[0]
|
||||
def string():
|
||||
return stream.readline().decode('latin1').strip()
|
||||
if stream.read(4) != b'tdef' or integer() != 1:
|
||||
raise ValueError(f'Unsupported tile format: {path}')
|
||||
count = integer()
|
||||
if not 0 <= count <= 10000:
|
||||
raise ValueError('Invalid tileset count')
|
||||
for _ in range(count):
|
||||
name, _image = string(), string()
|
||||
width, height, number = integer(), integer(), integer()
|
||||
for index in range(integer()):
|
||||
properties = {}
|
||||
for _ in range(integer()):
|
||||
key, value = string(), string()
|
||||
properties[key] = value
|
||||
out[f'{name}_{index}'] = properties
|
||||
if stream.read():
|
||||
raise ValueError(f'Unparsed tile data: {path}')
|
||||
return out
|
||||
|
||||
def geometry(path):
|
||||
text = path.read_text(encoding='utf-8-sig')
|
||||
# Canonical comments identify tiles; do not mistake a properties-only block for geometry.
|
||||
blocks = {}
|
||||
for m in re.finditer(r'/\*\s*(\w+_\d+)\s*\*/\s*tile\s*\{(.*?)(?=/\*|\Z)', text, re.S):
|
||||
if SHAPES.search(m[2]):
|
||||
body = re.sub(r'properties\s*\{[^{}]*\}', '', m[2])
|
||||
body = re.sub(r'xy\s*=\s*[^,]+,', '', body)
|
||||
# Remove closing tileset/root braces from the final tile in a sheet.
|
||||
body = body[:body.rfind('}')].strip()
|
||||
blocks[m[1]] = re.sub(r'\s+', '', body).rstrip('}')
|
||||
return blocks
|
||||
|
||||
def load_bindings(workshop):
|
||||
mod = workshop/'3810302175/mods/PZVoxelStudioViewpoint'
|
||||
root = mod/'common/media/voxel-studio'
|
||||
registration = (mod/'common/media/lua/client/PZVoxelStudioViewpoint.lua').read_text(encoding='utf-8-sig')
|
||||
manifests = re.findall(r'"media/voxel-studio/([^"\r\n]+\.properties)"', registration)
|
||||
if not manifests:
|
||||
raise ValueError('No registered model manifests found')
|
||||
bindings = {}
|
||||
for name in manifests:
|
||||
entries = {}
|
||||
for line in (root/name).read_text(encoding='utf-8-sig').splitlines():
|
||||
if line.strip() and not line.lstrip().startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
entries[key.strip()] = value.strip()
|
||||
models = {k[6:]:v for k,v in entries.items() if k.startswith('model.')}
|
||||
for key, value in entries.items():
|
||||
if not key.startswith('bind.'):
|
||||
continue
|
||||
fields = value.split(',')
|
||||
if len(fields) != 6 or fields[0] not in models:
|
||||
raise ValueError(f'Invalid binding {key} in {name}')
|
||||
[float(v) for v in fields[1:]]
|
||||
bindings[key[5:]] = dict(value=value, model=fields[0], path=models[fields[0]], manifest=name)
|
||||
return root, bindings
|
||||
|
||||
def load_catalogue(workshop):
|
||||
templates, raw, sources, patches = {}, {}, {}, {}
|
||||
for folder in ('KnoxBuildworks', 'KnoxBuildworks_Vanilla_Expanded'):
|
||||
root = workshop/'3772269882/mods'/folder/'42'
|
||||
manifest = read_json(root/'media/KnoxBuildworks/manifest.json')
|
||||
for relative in manifest['definitions']:
|
||||
data = read_json(root/relative)
|
||||
templates.update(data.get('templates', {}))
|
||||
for key, value in data.get('patches', {}).items():
|
||||
patches[key] = merge(patches.get(key,{}), value)
|
||||
for item in data.get('buildables', []):
|
||||
raw[item['id']] = item
|
||||
sources[item['id']] = f'{folder}/{relative}'
|
||||
def resolve(value, seen=()):
|
||||
parent = value.get('extends')
|
||||
if not parent:
|
||||
return copy.deepcopy(value)
|
||||
if parent in seen or parent not in templates:
|
||||
raise ValueError(f'Unknown or cyclic template: {parent}')
|
||||
return merge(resolve(templates[parent], (*seen, parent)), value)
|
||||
out = []
|
||||
for key,value in raw.items():
|
||||
item = resolve(value)
|
||||
item = merge(item, patches.get(key,{}))
|
||||
for stage in item.get('stages',[]):
|
||||
if stage.get('sprites'):
|
||||
continue
|
||||
directional = stage.get('directionalSprites',item.get('directionalSprites'))
|
||||
if directional:
|
||||
stage['sprites'] = copy.deepcopy(directional)
|
||||
continue
|
||||
pattern = stage.get('spritePattern',item.get('spritePattern'))
|
||||
if pattern:
|
||||
prefix = pattern.get('prefix',item.get('spritePrefix',''))
|
||||
suffix = pattern.get('suffix',item.get('spriteSuffix',''))
|
||||
expanded={d:prefix+str(token)+suffix for d,token in pattern.get('directions',{}).items()}
|
||||
if 'start' in pattern:
|
||||
directions=stage.get('rotations',item.get('rotations',['W','N','E','S']))
|
||||
expanded.update({d:prefix+str(pattern['start']+i*pattern.get('step',1))+suffix for i,d in enumerate(directions)})
|
||||
stage['sprites']=expanded
|
||||
item['_source'] = sources[key]
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
def sprite_strings(value):
|
||||
if isinstance(value, str) and SPRITE.fullmatch(value):
|
||||
yield value
|
||||
elif isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
if key in ('id','name','displayName','translationKey','tooltipKey','requirements','construction','tags'):
|
||||
continue
|
||||
yield from sprite_strings(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
yield from sprite_strings(child)
|
||||
|
||||
def braced_body(text, start):
|
||||
depth=1
|
||||
for index in range(start,len(text)):
|
||||
if text[index]=='{': depth+=1
|
||||
elif text[index]=='}':
|
||||
depth-=1
|
||||
if depth==0: return text[start:index]
|
||||
raise ValueError('Unbalanced entity script block')
|
||||
|
||||
def load_entity_sprites(game, workshop):
|
||||
entities={}
|
||||
roots=[game/'scripts', workshop/'3772269882/mods/KnoxBuildworks/common/media/scripts',
|
||||
workshop/'3772269882/mods/KnoxBuildworks/42/media/scripts']
|
||||
for root in roots:
|
||||
if not root.exists(): continue
|
||||
for path in sorted(root.rglob('*.txt')):
|
||||
text=path.read_text(encoding='utf-8-sig',errors='replace')
|
||||
if not re.search(r'\bentity\s+',text): continue
|
||||
text=re.sub(r'/\*.*?\*/|//[^\n]*','',text,flags=re.S)
|
||||
module=re.search(r'\bmodule\s+([\w.]+)\s*\{',text)
|
||||
if not module: continue
|
||||
for match in re.finditer(r'\bentity\s+([\w.]+)\s*\{',text):
|
||||
body=braced_body(text,match.end())
|
||||
entities[f'{module[1]}.{match[1]}']=set()
|
||||
sprite_config=re.search(r'\bcomponent\s+SpriteConfig\s*\{',body)
|
||||
if sprite_config:
|
||||
config=braced_body(body,sprite_config.end())
|
||||
sprites=set()
|
||||
for row in re.findall(r'\brow\s*=\s*([^,\n]+)',config):
|
||||
sprites.update(s for s in row.split() if SPRITE.fullmatch(s))
|
||||
entities[f'{module[1]}.{match[1]}']=sprites
|
||||
return entities
|
||||
|
||||
def entity_references(value):
|
||||
if isinstance(value,dict):
|
||||
ref=value.get('entityCompat')
|
||||
if isinstance(ref,dict) and ref.get('entity'):
|
||||
yield ref.get('module','Base')+'.'+ref['entity']
|
||||
for child in value.values(): yield from entity_references(child)
|
||||
elif isinstance(value,list):
|
||||
for child in value: yield from entity_references(child)
|
||||
|
||||
class Textures:
|
||||
"""Read pack metadata without loading the game; keep at most two atlas pages in memory."""
|
||||
def __init__(self, paths):
|
||||
self.index = {}
|
||||
self.pages = collections.OrderedDict()
|
||||
for path in paths:
|
||||
with path.open('rb') as stream:
|
||||
def integer(): return struct.unpack('<I', stream.read(4))[0]
|
||||
def string(): return stream.read(integer()).decode('latin1')
|
||||
if stream.read(4) != b'PZPK' or integer() != 1:
|
||||
raise ValueError(f'Unsupported texture pack: {path}')
|
||||
for _ in range(integer()):
|
||||
page, count, alpha = string(), integer(), integer()
|
||||
subs = []
|
||||
for _ in range(count):
|
||||
name = string()
|
||||
subs.append((name, struct.unpack('<8i', stream.read(32))))
|
||||
length = integer()
|
||||
position = stream.tell()
|
||||
stream.seek(length, 1)
|
||||
for name, rect in subs:
|
||||
self.index[name] = dict(file=path, position=position, length=length, rect=rect)
|
||||
if stream.read():
|
||||
raise ValueError(f'Unparsed texture data: {path}')
|
||||
|
||||
def sprite(self, name):
|
||||
row = self.index[name]
|
||||
key = row['file'], row['position']
|
||||
if key not in self.pages:
|
||||
with row['file'].open('rb') as stream:
|
||||
stream.seek(row['position'])
|
||||
self.pages[key] = Image.open(io.BytesIO(stream.read(row['length']))).convert('RGBA')
|
||||
while len(self.pages) > 2:
|
||||
self.pages.popitem(last=False)
|
||||
self.pages.move_to_end(key)
|
||||
x,y,w,h,*_ = row['rect']
|
||||
return self.pages[key].crop((x,y,x+w,y+h))
|
||||
|
||||
def fingerprints(self, names):
|
||||
groups = collections.defaultdict(list)
|
||||
out = {}
|
||||
for name in set(names) & self.index.keys():
|
||||
row = self.index[name]
|
||||
groups[(row['file'], row['position'])].append(name)
|
||||
for group in groups.values():
|
||||
for name in group:
|
||||
image = self.sprite(name)
|
||||
box = image.getbbox()
|
||||
if not box:
|
||||
continue
|
||||
image = image.crop(box)
|
||||
# Ignore invisible RGB bytes, while retaining alpha and object anchor.
|
||||
clean = Image.new('RGBA', image.size)
|
||||
clean.paste(image, mask=image.getchannel('A').point(lambda a: 255 if a else 0))
|
||||
x,y,w,h,ox,oy,fx,fy = self.index[name]['rect']
|
||||
out[name] = (image.size, ox+box[0], oy+box[1], fx, fy, hashlib.sha256(clean.tobytes()).hexdigest())
|
||||
return out
|
||||
|
||||
def semantic_conflict(a, b):
|
||||
return any(a.get(k) and b.get(k) and a[k] != b[k] for k in SEMANTICS)
|
||||
|
||||
def alias_matches(sprite, bindings, properties):
|
||||
if not sprite.startswith('kbw_'):
|
||||
return None
|
||||
source = sprite[4:]
|
||||
if source not in bindings:
|
||||
return None
|
||||
a,b = properties.get(sprite,{}), properties.get(source,{})
|
||||
# Same index is not sufficient. In particular Knox white crates reuse vanilla sign indices.
|
||||
if semantic_conflict(a,b):
|
||||
return None
|
||||
if not a.get('CustomName') or a.get('CustomName') != b.get('CustomName'):
|
||||
return None
|
||||
if a.get('container') != b.get('container'):
|
||||
return None
|
||||
return source
|
||||
|
||||
def native_route(sprite, properties, shapes):
|
||||
p = properties.get(sprite,{})
|
||||
if 'solidfloor' in p:
|
||||
return 'native-floor', 'Game solidfloor flag; Viewpoint floor renderer'
|
||||
if sprite.startswith(('roofs_', 'roofs_accents_')):
|
||||
return 'native-roof-candidate', 'Roof tile family; uses Viewpoint roof system rather than a furniture binding; visual check needed'
|
||||
if sprite in shapes:
|
||||
return 'native-geometry', 'Installed tileGeometry.txt contains box/polygon/cylinder geometry'
|
||||
if any(k in p for k in ('FloorOverlay','attachedFloor')):
|
||||
return 'native-floor-overlay', 'Game floor overlay metadata; Viewpoint floor/decal renderer'
|
||||
edge_flags = ('cutN','cutW','collideN','collideW','HoppableN','HoppableW','TallHoppableN','TallHoppableW',
|
||||
'transparentN','transparentW','WallN','WallW','WallNW','WallNTrans','WallWTrans','WallNWTrans',
|
||||
'DoorWallN','DoorWallW','WindowN','WindowW','doorN','doorW','windowN','windowW')
|
||||
if any(k in p for k in edge_flags):
|
||||
return 'native-edge', 'Game wall/door/window/fence edge flags; keep Viewpoint edge rendering'
|
||||
return None
|
||||
|
||||
def candidates(sprite, bindings, properties, shapes):
|
||||
p = properties.get(sprite,{})
|
||||
out = []
|
||||
for source, binding in bindings.items():
|
||||
q = properties.get(source,{})
|
||||
same_family = family(sprite) == family(source)
|
||||
named = p.get('CustomName') and p.get('CustomName') == q.get('CustomName')
|
||||
if not (same_family or named) or semantic_conflict(p,q):
|
||||
continue
|
||||
reasons, score = [],0
|
||||
if same_family: score += 20; reasons.append('same tile family')
|
||||
if named: score += 25; reasons.append('same CustomName')
|
||||
if p.get('GroupName') and p.get('GroupName') == q.get('GroupName'):
|
||||
score += 20; reasons.append('same GroupName')
|
||||
if p.get('Facing') and p.get('Facing') == q.get('Facing'):
|
||||
score += 10; reasons.append('same facing')
|
||||
if p.get('MaterialType') and p.get('MaterialType') == q.get('MaterialType'):
|
||||
score += 5; reasons.append('same material')
|
||||
if sprite in shapes and shapes.get(sprite) == shapes.get(source):
|
||||
score += 20; reasons.append('same native geometry')
|
||||
if score >= 25:
|
||||
out.append(dict(source=source, score=score, reasons=reasons))
|
||||
# This deliberately ranks suggestions rather than applying shape/color guesses.
|
||||
return sorted(out, key=lambda r:(-r['score'],r['source']))[:3]
|
||||
|
||||
def tint_texture(image, color):
|
||||
"""Set the dominant painted surface to the requested RGB; preserve relative luminance and alpha."""
|
||||
if not re.fullmatch(r'#[0-9a-fA-F]{6}',color):
|
||||
raise ValueError(f'Invalid texture tint: {color}')
|
||||
target=tuple(int(color[i:i+2],16) for i in (1,3,5))
|
||||
image=image.convert('RGBA')
|
||||
colors=collections.Counter()
|
||||
raw=image.tobytes()
|
||||
for pixel,count in collections.Counter(raw[i:i+4] for i in range(0,len(raw),4)).items():
|
||||
r,g,b,a=pixel
|
||||
if a>=128 and g>r and g>b and max(r,g,b)>25:
|
||||
colors[(r,g,b)]+=count
|
||||
if not colors:
|
||||
raise ValueError('The requested green source atlas has no painted green surface')
|
||||
base=colors.most_common(1)[0][0]
|
||||
reference=Image.new('RGB',(1,1),base).convert('L').getpixel((0,0))
|
||||
luminance=image.convert('L')
|
||||
channels=[luminance.point([min(255,round(channel*value/reference)) for value in range(256)]) for channel in target]
|
||||
return Image.merge('RGBA',(*channels,image.getchannel('A')))
|
||||
|
||||
def copy_asset(source_root, binding, media, white=False, tint=None):
|
||||
source = (source_root/binding['path']).resolve()
|
||||
source.relative_to(source_root.resolve())
|
||||
tag = ('white-' if white else ('tint-'+tint[1:].lower()+'-' if tint else '')) + hashlib.sha256(str(source).encode()).hexdigest()[:16]
|
||||
target = media/'knox-crates-assets'/tag/source.name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source,target)
|
||||
for line in source.read_text(encoding='utf-8-sig').splitlines():
|
||||
if not line.startswith('mtllib '):
|
||||
continue
|
||||
name = line.split(None,1)[1].strip()
|
||||
material = (source.parent/name).resolve()
|
||||
material.relative_to(source_root.resolve())
|
||||
destination = target.parent/name
|
||||
destination.parent.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copy2(material,destination)
|
||||
for ml in material.read_text(encoding='utf-8-sig').splitlines():
|
||||
if re.match(r'^map_\w+\s+',ml):
|
||||
texture_name = ml.split(None,1)[1].strip()
|
||||
texture = (material.parent/texture_name).resolve()
|
||||
texture.relative_to(source_root.resolve())
|
||||
texture_out = destination.parent/texture_name
|
||||
texture_out.parent.mkdir(parents=True,exist_ok=True)
|
||||
if tint:
|
||||
tint_texture(Image.open(texture),tint).save(texture_out)
|
||||
elif white:
|
||||
# Reviewed white crate adaptation: reuse crate geometry and UVs, retain atlas alpha/detail.
|
||||
image = Image.open(texture).convert('RGBA')
|
||||
alpha = image.getchannel('A')
|
||||
luminance = image.convert('L').point(lambda v: min(245, int(115 + v*0.65)))
|
||||
Image.merge('RGBA',(luminance,luminance,luminance,alpha)).save(texture_out)
|
||||
else:
|
||||
shutil.copy2(texture,texture_out)
|
||||
return target.relative_to(media).as_posix(), tag
|
||||
|
||||
def mod_info(versioned=False):
|
||||
prefix='../' if versioned else ''
|
||||
art=f'{prefix}common/media/textures/preview.png'
|
||||
lines=['name=Knox Buildworks - Viewpoint Compatibility',f'id={MOD_ID}',f'poster={art}','',
|
||||
'description=3D military crate models for Knox Buildworks in Project Viewpoint.',
|
||||
'author=Riggs0','category=building']
|
||||
if versioned:
|
||||
lines += [f'icon={art}','url=https://hudsonriggs.systems','modversion=2.1.0','versionMin=42.20.0']
|
||||
else:
|
||||
lines += ['versionMin=42.20.0','url=https://hudsonriggs.systems','modversion=2.1.0',f'icon={art}']
|
||||
lines += ['require=KnoxBuildworks,Viewpoint,PZVoxelStudioViewpoint',
|
||||
'loadAfter=KnoxBuildworks,PZVoxelStudioViewpoint,Viewpoint','']
|
||||
return '\n'.join(lines)
|
||||
|
||||
def build_pack(output, source_root, bindings, assignments):
|
||||
media=output/'common/media'
|
||||
media.mkdir(parents=True,exist_ok=True)
|
||||
lines=['id=knox-buildworks-viewpoint-crates','name=Knox Buildworks - Viewpoint Compatibility','version=2']
|
||||
models={}
|
||||
for target, assignment in sorted(assignments.items()):
|
||||
binding=bindings[assignment['source']]
|
||||
white=assignment.get('adaptation')=='white-crate'
|
||||
tint=assignment.get('tint')
|
||||
key=(binding['path'],white,tint)
|
||||
if key not in models:
|
||||
relative,tag=copy_asset(source_root,binding,media,white,tint)
|
||||
model='kbw-'+tag
|
||||
models[key]=model
|
||||
lines.append(f'model.{model}={relative}')
|
||||
_,*transform=binding['value'].split(',')
|
||||
lines.append(f"bind.{target}={models[key]},{','.join(transform)}")
|
||||
(media/'knox-viewpoint-crates.properties').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
(output/VERSION_FOLDER).mkdir(parents=True,exist_ok=True)
|
||||
(output/VERSION_FOLDER/'mod.info').write_text(mod_info(versioned=True),encoding='utf-8')
|
||||
(output/'mod.info').write_text(mod_info(),encoding='utf-8')
|
||||
# Keep the published preview and mod-list art consistent on future rebuilds.
|
||||
preview=output/'preview.png'
|
||||
if not preview.exists():
|
||||
preview=REPO_ROOT/'preview.png'
|
||||
if preview.exists():
|
||||
art=media/'textures/preview.png'
|
||||
art.parent.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copy2(preview,art)
|
||||
lua='''-- Render bindings only. No recipe, object, inventory or save changes.
|
||||
local registered = false
|
||||
local warned = false
|
||||
local function register()
|
||||
if registered or not Viewpoint or not Viewpoint.ModelPacks then return end
|
||||
local ok, result = pcall(Viewpoint.ModelPacks.register,
|
||||
"%MOD_ID%", "media/knox-viewpoint-crates.properties")
|
||||
if ok and result then
|
||||
registered = true
|
||||
print("[Knox Viewpoint Crates] Registered %COUNT% reviewed compatibility sprite bindings.")
|
||||
elseif not warned then
|
||||
warned = true
|
||||
print("[Knox Viewpoint Crates] Model pack registration failed: " .. tostring(result))
|
||||
end
|
||||
end
|
||||
Events.OnGameBoot.Add(register)
|
||||
Events.OnGameStart.Add(register)
|
||||
register()
|
||||
'''.replace('%MOD_ID%',MOD_ID).replace('%COUNT%',str(len(assignments)))
|
||||
(media/'lua/client').mkdir(parents=True,exist_ok=True)
|
||||
(media/'lua/client/ZZ_KnoxViewpointCrates.lua').write_text(lua,encoding='utf-8')
|
||||
return len(models)
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--workshop-root',type=Path,default=Path('D:/SteamLibrary/steamapps/workshop/content/108600'))
|
||||
parser.add_argument('--game-directory',type=Path,default=Path('D:/SteamLibrary/steamapps/common/ProjectZomboid'))
|
||||
parser.add_argument('--output-directory',type=Path,default=REPO_ROOT)
|
||||
parser.add_argument('--reports-directory',type=Path,default=REPO_ROOT/'reports')
|
||||
parser.add_argument('--install',action='store_true')
|
||||
parser.add_argument('--local-mods-directory',type=Path,default=Path.home()/'Zomboid/mods')
|
||||
args=parser.parse_args()
|
||||
game=args.game_directory/'media'
|
||||
source_root,bindings=load_bindings(args.workshop_root)
|
||||
catalogue=load_catalogue(args.workshop_root)
|
||||
entities=load_entity_sprites(game,args.workshop_root)
|
||||
properties={}
|
||||
for path in [game/'newtiledefinitions.tiles',game/'tiledefinitions_erosion.tiles',game/'tiledefinitions_noiseworks.patch.tiles',
|
||||
args.workshop_root/'3772269882/mods/KnoxBuildworks/common/media/knox_buildworks_pack.tiles']:
|
||||
for name,p in tile_properties(path).items(): properties.setdefault(name,{}).update(p)
|
||||
shapes=geometry(game/'tileGeometry.txt')
|
||||
shapes.update(geometry(args.workshop_root/'3772269882/mods/KnoxBuildworks/common/media/tileGeometry.txt'))
|
||||
all_sprites=set()
|
||||
object_sprites={}
|
||||
unresolved_entities={}
|
||||
for item in catalogue:
|
||||
# Include geometry/states/finishes/variants/materials inherited from templates; exclude mere icons.
|
||||
relevant={k:item[k] for k in ('stages','geometry','sprites','finishes','variants','materials') if k in item}
|
||||
object_sprites[item['id']]=set(sprite_strings(relevant))
|
||||
for entity in set(entity_references(item)):
|
||||
if entity in entities:
|
||||
object_sprites[item['id']].update(entities[entity])
|
||||
else:
|
||||
unresolved_entities.setdefault(item['id'],[]).append(entity)
|
||||
all_sprites.update(object_sprites[item['id']])
|
||||
textures=Textures([game/'texturepacks/Tiles2x.pack',game/'texturepacks/Tiles2x.floor.pack',
|
||||
args.workshop_root/'3772269882/mods/KnoxBuildworks/common/media/texturepacks/knox_buildworks_pack.pack'])
|
||||
print(f'Read {len(catalogue)} catalogue entries, {len(all_sprites)} sprites and {len(bindings)} authored bindings.',flush=True)
|
||||
fingerprints=textures.fingerprints(all_sprites | bindings.keys())
|
||||
exact=collections.defaultdict(list)
|
||||
for name in bindings:
|
||||
if name in fingerprints: exact[fingerprints[name]].append(name)
|
||||
assignments={}
|
||||
decisions={}
|
||||
manual=read_json(HERE/'reviewed-matches.json')
|
||||
for name in sorted(all_sprites):
|
||||
p=properties.get(name,{})
|
||||
if name in bindings:
|
||||
decisions[name]=dict(status='already-bound',source=name,evidence='Exact authored sprite binding')
|
||||
continue
|
||||
# Explicit reviewed choices override generic matching (including user-requested recolors).
|
||||
alias=alias_matches(name,bindings,properties) if name not in manual['matches'] else None
|
||||
if alias:
|
||||
assignments[name]=dict(source=alias,method='automatic-alias')
|
||||
decisions[name]=dict(status='automatic-match',source=alias,evidence='Prefix alias with matching object/container semantics')
|
||||
continue
|
||||
identical=[s for s in exact.get(fingerprints.get(name),[]) if not semantic_conflict(p,properties.get(s,{})) and family(name)==family(s)]
|
||||
identities={(bindings[s]['path'],bindings[s]['value'].split(',',1)[1]) for s in identical}
|
||||
if identical and len(identities)==1 and name not in manual['matches']:
|
||||
source=sorted(identical)[0]
|
||||
assignments[name]=dict(source=source,method='automatic-identical-art')
|
||||
decisions[name]=dict(status='automatic-match',source=source,evidence='Identical anchored RGBA artwork, same tile family and compatible metadata')
|
||||
continue
|
||||
if name in manual['matches']:
|
||||
review=manual['matches'][name]
|
||||
if review['source'] not in bindings:
|
||||
raise ValueError(f"Reviewed source disappeared: {review['source']}")
|
||||
# Abort when either target/source meaning changes after a Workshop update.
|
||||
expected=review.get('expectedTargetCustomName')
|
||||
if expected and p.get('CustomName')!=expected:
|
||||
raise ValueError(f'Reviewed target changed: {name}')
|
||||
expected_source=review.get('expectedSourceCustomName')
|
||||
if expected_source and properties.get(review['source'],{}).get('CustomName')!=expected_source:
|
||||
raise ValueError(f'Reviewed source changed: {name}')
|
||||
assignments[name]=dict(source=review['source'],method='manual-adaptation',adaptation=review.get('adaptation'),tint=review.get('tint'))
|
||||
decisions[name]=dict(status='manual-adaptation',**review)
|
||||
continue
|
||||
native=native_route(name,properties,shapes)
|
||||
if native:
|
||||
decisions[name]=dict(status=native[0],evidence=native[1])
|
||||
continue
|
||||
suggestions=candidates(name,bindings,properties,shapes)
|
||||
rule=next((r for r in manual['unmatchedReviews'] if re.fullmatch(r['pattern'],name)),None)
|
||||
decisions[name]=dict(status='no-compatible-authored-model',candidates=suggestions,
|
||||
evidence=rule['reason'] if rule else 'No exact or reviewed compatible asset; do not force a different object model',
|
||||
reviewed=bool(rule))
|
||||
# Include custom stacked sprites which are managed by the game's stack system, not just recipes.
|
||||
for name,review in manual.get('extraSprites',{}).items():
|
||||
if name not in properties: raise ValueError(f'Missing reviewed stack sprite: {name}')
|
||||
if review['source'] not in bindings: raise ValueError(f'Missing reviewed stack model: {review["source"]}')
|
||||
if properties[name].get('CustomName') != 'Crate' or properties.get(review['source'],{}).get('CustomName') != 'Crate':
|
||||
raise ValueError(f'Reviewed stack meaning changed: {name}')
|
||||
assignments[name]=dict(source=review['source'],method=review['method'],adaptation=review.get('adaptation'),tint=review.get('tint'))
|
||||
decisions[name]=dict(status=review['method'],**review)
|
||||
model_count=build_pack(args.output_directory,source_root,bindings,assignments)
|
||||
counts=dict(collections.Counter(d['status'] for d in decisions.values()))
|
||||
objects=[]
|
||||
for item in catalogue:
|
||||
sprites=sorted(object_sprites[item['id']])
|
||||
counts_for_object=collections.Counter(decisions[s]['status'] for s in sprites)
|
||||
if not sprites: counts_for_object['no-static-sprite-reference']=1
|
||||
objects.append(dict(id=item['id'],name=item.get('displayName',item['id']),category=item.get('category',''),
|
||||
source=item['_source'],statuses=dict(counts_for_object),sprites=sprites,
|
||||
unresolvedEntityReferences=unresolved_entities.get(item['id'],[])))
|
||||
summary=dict(catalogueEntries=len(catalogue),entriesWithResolvedSprites=sum(bool(o['sprites']) for o in objects),uniqueSprites=len(decisions),
|
||||
authoredPackBindings=len(bindings),patchBindings=len(assignments),copiedModels=model_count,statusCounts=counts,
|
||||
note='Native routes describe installed geometry/metadata, not verified in-game appearance. Roof family matches require visual checks. No-compatible-model entries have no accepted model assignment.')
|
||||
summary['unresolvedEntityReferences']=unresolved_entities
|
||||
gap_objects=[o for o in objects if o['statuses'].get('no-compatible-authored-model',0)]
|
||||
summary['buildablesWithReviewedModelGaps']=len(gap_objects)
|
||||
summary['unreviewedGapSprites']=sum(d['status']=='no-compatible-authored-model' and not d.get('reviewed') for d in decisions.values())
|
||||
write_json(args.reports_directory/'summary.json',summary)
|
||||
write_json(args.reports_directory/'sprite-decisions.json',decisions)
|
||||
write_json(args.reports_directory/'buildable-coverage.json',objects)
|
||||
write_json(args.reports_directory/'applied-matches.json',assignments)
|
||||
lines=['# Matching results','',f"Scanned {len(catalogue)} catalogue entries; added {len(assignments)} sprite bindings.",'',
|
||||
'Native geometry is recorded separately from authored model matches. File analysis is not an in-game visual test.','',
|
||||
'## Applied matches','']
|
||||
for target,a in sorted(assignments.items()): lines.append(f"- `{target}` -> `{a['source']}` ({a['method']}{'; '+a['adaptation'] if a.get('adaptation') else ''})")
|
||||
lines+=['','## Buildables still without a complete authored model match','',
|
||||
'The reasons below identify native rendering routes and reviewed gaps. A native route is not an invisible object.','']
|
||||
for item in sorted(objects,key=lambda a:(a['name'],a['id'])):
|
||||
if not item['sprites']:
|
||||
lines.append(f"- **{item['name']}** — `{item['id']}`: no static sprite reference; game action/runtime entity; not assigned a model")
|
||||
continue
|
||||
other=[s for s in item['sprites'] if decisions[s]['status'] not in ('already-bound','automatic-match','manual-adaptation')]
|
||||
if not other: continue
|
||||
statuses=collections.Counter(decisions[s]['status'] for s in other)
|
||||
lines.append(f"- **{item['name']}** — `{item['id']}`: "+', '.join(f'{k}: {v}' for k,v in sorted(statuses.items())))
|
||||
for sprite in other:
|
||||
if decisions[sprite]['status']=='no-compatible-authored-model':
|
||||
lines.append(f" - `{sprite}`: {decisions[sprite]['evidence']}")
|
||||
(args.reports_directory/'results.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
gaps=['# Buildables without a complete accepted authored model match','',
|
||||
f'{len(gap_objects)} buildables have one or more unresolved authored-model parts. These are reviewed gaps, not confirmed invisible objects. Native edge/depth/overlay rendering may still display them.','',
|
||||
'The earlier audit counted ordinary native walls/floors/roofs as unmatched; those native routes are now recorded separately in results.md.','']
|
||||
for item in sorted(gap_objects,key=lambda o:(o['name'],o['id'])):
|
||||
gaps.append(f"## {item['name']}")
|
||||
gaps.extend(['',f"`{item['id']}`",''])
|
||||
for sprite in item['sprites']:
|
||||
if decisions[sprite]['status']=='no-compatible-authored-model':
|
||||
gaps.append(f"- `{sprite}`: {decisions[sprite]['evidence']}")
|
||||
gaps.append('')
|
||||
(args.reports_directory/'unmatched.md').write_text('\n'.join(gaps)+'\n',encoding='utf-8')
|
||||
write_json(args.output_directory/'match-summary.json',summary)
|
||||
if args.install:
|
||||
destination=args.local_mods_directory/MOD_ID
|
||||
if args.output_directory.resolve()!=destination.resolve():
|
||||
# The output can be a Git repository. Install runtime files only, never .git/tools/reports.
|
||||
destination.mkdir(parents=True,exist_ok=True)
|
||||
for directory in ('common',VERSION_FOLDER):
|
||||
shutil.copytree(args.output_directory/directory,destination/directory,dirs_exist_ok=True)
|
||||
for filename in ('mod.info','match-summary.json'):
|
||||
shutil.copy2(args.output_directory/filename,destination/filename)
|
||||
print(f'Installed {destination}',flush=True)
|
||||
print(json.dumps(summary,indent=2),flush=True)
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Meaningful regression checks for the matcher. Run with the same Python as the matcher."""
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
HERE=Path(__file__).resolve().parent
|
||||
sys.dont_write_bytecode=True
|
||||
spec=importlib.util.spec_from_file_location('matcher',HERE/'Match-KnoxViewpoint.py')
|
||||
matcher=importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(matcher)
|
||||
|
||||
class MatchingTests(unittest.TestCase):
|
||||
def test_white_crate_is_not_vanilla_military_sign(self):
|
||||
target='kbw_location_military_generic_01_16'
|
||||
bindings={'location_military_generic_01_16':{}}
|
||||
props={target:{'CustomName':'Crate','container':'militarycrate'},
|
||||
'location_military_generic_01_16':{'CustomName':'Sign'}}
|
||||
self.assertIsNone(matcher.alias_matches(target,bindings,props))
|
||||
|
||||
def test_real_crate_alias_matches(self):
|
||||
target='kbw_location_military_generic_01_0'
|
||||
props={target:{'CustomName':'Crate','container':'militarycrate'},
|
||||
target[4:]:{'CustomName':'Crate','container':'militarycrate'}}
|
||||
self.assertEqual(matcher.alias_matches(target,{target[4:]:{}},props),target[4:])
|
||||
|
||||
def test_properties_only_tile_has_no_geometry(self):
|
||||
text='''/* fixtures_test_0 */ tile { xy=0x0, properties { Translucent=true, } }
|
||||
/* fixtures_test_1 */ tile { xy=1x0, cylinder { min=0x0x0, max=1x1x1, } }'''
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path=Path(temp)/'tileGeometry.txt';path.write_text(text)
|
||||
result=matcher.geometry(path)
|
||||
self.assertNotIn('fixtures_test_0',result)
|
||||
self.assertIn('fixtures_test_1',result)
|
||||
|
||||
def test_variant_ids_are_not_sprites(self):
|
||||
value={'id':'tile_02','geometry':{'rows':[['walls_interior_house_01_2']]},'requirements':{'id':'tool_03'}}
|
||||
self.assertEqual(list(matcher.sprite_strings(value)),['walls_interior_house_01_2'])
|
||||
|
||||
def test_real_sprite_names_can_have_capitals_and_hyphens(self):
|
||||
self.assertTrue(matcher.SPRITE.fullmatch('location_shop_bargNclothes_01_24'))
|
||||
self.assertTrue(matcher.SPRITE.fullmatch('signs_one-off_08_269'))
|
||||
|
||||
def test_native_floor_does_not_need_authored_model(self):
|
||||
result=matcher.native_route('floors_test_0',{'floors_test_0':{'solidfloor':''}}, {})
|
||||
self.assertEqual(result[0],'native-floor')
|
||||
|
||||
def test_native_roof_is_candidate_not_visual_confirmation(self):
|
||||
self.assertEqual(matcher.native_route('roofs_01_0',{}, {})[0],'native-roof-candidate')
|
||||
|
||||
def test_suggestions_never_change_assignments(self):
|
||||
bindings={'fixtures_test_1':{}}
|
||||
props={'fixtures_test_0':{'CustomName':'Shelf'},'fixtures_test_1':{'CustomName':'Shelf'}}
|
||||
result=matcher.candidates('fixtures_test_0',bindings,props,{})
|
||||
self.assertEqual(result[0]['source'],'fixtures_test_1')
|
||||
self.assertEqual(bindings,{'fixtures_test_1':{}})
|
||||
|
||||
def test_registered_manifest_order_and_source_paths(self):
|
||||
workshop=Path('D:/SteamLibrary/steamapps/workshop/content/108600')
|
||||
if not workshop.exists(): self.skipTest('Installed Workshop files not present')
|
||||
root,bindings=matcher.load_bindings(workshop)
|
||||
self.assertEqual(bindings['location_military_generic_01_1']['manifest'],'package-12.properties')
|
||||
self.assertTrue((root/bindings['location_military_generic_01_1']['path']).is_file())
|
||||
|
||||
def test_white_adaptation_preserves_mesh_and_alpha_and_source(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root=Path(temp)/'source';root.mkdir()
|
||||
obj=root/'model.obj';obj.write_text('mtllib model.mtl\nv 0 0 0\n')
|
||||
(root/'model.mtl').write_text('newmtl atlas\nmap_Kd texture.png\n')
|
||||
image=Image.new('RGBA',(2,1));image.putdata([(20,40,60,255),(80,90,100,0)]);image.save(root/'texture.png')
|
||||
before=(root/'texture.png').read_bytes()
|
||||
media=Path(temp)/'patch/media'
|
||||
relative,tag=matcher.copy_asset(root,{'path':'model.obj'},media,white=True)
|
||||
self.assertEqual((media/relative).read_bytes(),obj.read_bytes())
|
||||
after=Image.open((media/relative).parent/'texture.png').convert('RGBA')
|
||||
self.assertEqual(after.getchannel('A').tobytes(),bytes([255,0]))
|
||||
r,g,b,a=after.getpixel((0,0));self.assertEqual(r,g);self.assertEqual(g,b);self.assertGreater(r,100)
|
||||
self.assertEqual((root/'texture.png').read_bytes(),before)
|
||||
|
||||
def test_manual_white_sources_are_crates_not_signs(self):
|
||||
reviews=json.loads((HERE/'reviewed-matches.json').read_text())
|
||||
self.assertEqual(reviews['matches']['kbw_location_military_generic_01_16']['source'],'location_military_generic_01_8')
|
||||
self.assertEqual(reviews['matches']['kbw_location_military_generic_01_17']['source'],'location_military_generic_01_9')
|
||||
|
||||
def test_gray_variants_use_green_meshes_and_requested_tint(self):
|
||||
reviews=json.loads((HERE/'reviewed-matches.json').read_text())
|
||||
rules={**reviews['matches'],**reviews['extraSprites']}
|
||||
for target,source in zip((8,9,10,11,12,13),(0,1,2,3,4,5)):
|
||||
rule=rules[f'kbw_location_military_generic_01_{target}']
|
||||
self.assertEqual(rule['source'],f'location_military_generic_01_{source}')
|
||||
self.assertEqual(rule['tint'],'#626763')
|
||||
|
||||
def test_gray_tint_exact_base_shading_and_alpha(self):
|
||||
original=Image.new('RGBA',(4,1))
|
||||
original.putdata([(85,97,74,255),(85,97,74,255),(42,48,37,255),(85,97,74,0)])
|
||||
tinted=matcher.tint_texture(original,'#626763')
|
||||
self.assertEqual(tinted.getpixel((0,0)),(98,103,99,255))
|
||||
self.assertLess(tinted.getpixel((2,0))[0],98)
|
||||
self.assertEqual(tinted.getchannel('A').tobytes(),original.getchannel('A').tobytes())
|
||||
self.assertEqual(original.getpixel((0,0)),(85,97,74,255))
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1 @@
|
||||
Pillow>=10
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"reviewBasis": "Reviewed installed tile properties, actual 2D sprite contact sheets, model manifests and Viewpoint renderer bytecode. Approve only object-compatible model reuse. Never infer identity from the tile number alone.",
|
||||
"matches": {
|
||||
"kbw_location_military_generic_01_8": {
|
||||
"source": "location_military_generic_01_0",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763",
|
||||
"expectedTargetCustomName": "Crate",
|
||||
"expectedSourceCustomName": "Crate",
|
||||
"evidence": "User-requested gray crate appearance: use the matching green crate mesh, retint its atlas toward #626763, and preserve luminance detail and transparency."
|
||||
},
|
||||
"kbw_location_military_generic_01_9": {
|
||||
"source": "location_military_generic_01_1",
|
||||
"adaptation": "gray-green-crate",
|
||||
"tint": "#626763",
|
||||
"expectedTargetCustomName": "Crate",
|
||||
"expectedSourceCustomName": "Crate",
|
||||
"evidence": "Second gray crate orientation uses the second green crate mesh with the same requested gray-green base tint."
|
||||
},
|
||||
"kbw_location_military_generic_01_16": {
|
||||
"source": "location_military_generic_01_8",
|
||||
"adaptation": "white-crate",
|
||||
"expectedTargetCustomName": "Crate",
|
||||
"expectedSourceCustomName": "Crate",
|
||||
"evidence": "Knox white crate has the same military-crate shape as gray crate orientation 8. Reuse that crate mesh with a neutral white atlas. Vanilla index 16 is a military sign and is explicitly rejected. Color is an approximation; custom paint tint is not supplied by this pack."
|
||||
},
|
||||
"kbw_location_military_generic_01_17": {
|
||||
"source": "location_military_generic_01_9",
|
||||
"adaptation": "white-crate",
|
||||
"expectedTargetCustomName": "Crate",
|
||||
"expectedSourceCustomName": "Crate",
|
||||
"evidence": "Same reviewed military-crate mesh for the second orientation; neutral white atlas. Vanilla index 17 is a sign, not a crate."
|
||||
}
|
||||
},
|
||||
"extraSprites": {
|
||||
"kbw_location_military_generic_01_2": {"source":"location_military_generic_01_2","method":"reviewed-stack"},
|
||||
"kbw_location_military_generic_01_3": {"source":"location_military_generic_01_3","method":"reviewed-stack"},
|
||||
"kbw_location_military_generic_01_4": {"source":"location_military_generic_01_4","method":"reviewed-stack"},
|
||||
"kbw_location_military_generic_01_5": {"source":"location_military_generic_01_5","method":"reviewed-stack"},
|
||||
"kbw_location_military_generic_01_10": {"source":"location_military_generic_01_2","method":"reviewed-stack","adaptation":"gray-green-crate","tint":"#626763"},
|
||||
"kbw_location_military_generic_01_11": {"source":"location_military_generic_01_3","method":"reviewed-stack","adaptation":"gray-green-crate","tint":"#626763"},
|
||||
"kbw_location_military_generic_01_12": {"source":"location_military_generic_01_4","method":"reviewed-stack","adaptation":"gray-green-crate","tint":"#626763"},
|
||||
"kbw_location_military_generic_01_13": {"source":"location_military_generic_01_5","method":"reviewed-stack","adaptation":"gray-green-crate","tint":"#626763"},
|
||||
"kbw_location_military_generic_01_18": {"source":"location_military_generic_01_10","method":"reviewed-stack","adaptation":"white-crate"},
|
||||
"kbw_location_military_generic_01_19": {"source":"location_military_generic_01_11","method":"reviewed-stack","adaptation":"white-crate"},
|
||||
"kbw_location_military_generic_01_20": {"source":"location_military_generic_01_12","method":"reviewed-stack","adaptation":"white-crate"},
|
||||
"kbw_location_military_generic_01_21": {"source":"location_military_generic_01_13","method":"reviewed-stack","adaptation":"white-crate"}
|
||||
},
|
||||
"unmatchedReviews": [
|
||||
{"pattern":"overlay_graffiti_wall_.*", "reason":"Reviewed wall-paint/sign overlay family. These are surface artwork, not standalone 3D objects. No matching authored mesh is needed to retain the art on its wall, and substituting a furniture mesh would be incorrect."},
|
||||
{"pattern":"fixtures_bathroom_.*", "reason":"Reviewed attached bathroom fixture/overlay. No accepted authored asset with matching dimensions and artwork; retain native attachment behavior."},
|
||||
{"pattern":"walls_.*", "reason":"Reviewed architectural wall/trim/pillar family. No accepted authored asset with matching shape and artwork; many variants rely on cut/edge/depth rendering. Do not substitute a furniture mesh or a differently shaped window wall."},
|
||||
{"pattern":"fixtures_doors_.*", "reason":"Reviewed doors/open states/frame trims. Similar names or adjacent indices do not prove the same door or opening angle. Preserve Viewpoint door rendering; no accepted replacement asset."},
|
||||
{"pattern":"fixtures_windows_.*", "reason":"Reviewed specialized window shape. Other window meshes are not guaranteed to match this shape, facing or broken/open state. Preserve native rendering; bespoke geometry would be needed for a faithful authored replacement."},
|
||||
{"pattern":"fixtures_railings_.*", "reason":"Reviewed railing contact sheet. Unmatched sloped stair tips/posts are distinct sections of multi-tile railings, not duplicate full railing models. Substituting a full model here would duplicate or misalign the railing."},
|
||||
{"pattern":"fixtures_fireplaces_.*", "reason":"Reviewed double-fireplace sprite footprint. Its halves form a two-tile opening; other fireplace/chimney models have different dimensions. Preserve existing edge/depth rendering rather than place a whole fireplace on each half."},
|
||||
{"pattern":"signs_.*", "reason":"Reviewed sign family. No accepted authored model with the target sign artwork/size. Similar sign geometry alone would display a different business or sign."},
|
||||
{"pattern":"location_.*", "reason":"Reviewed location-specific facade/sign/fixture family. No accepted equivalent model with this artwork and footprint; no cross-object substitution applied."},
|
||||
{"pattern":"fencing_.*", "reason":"Reviewed fence and corner sections. Distinct corner/end geometry must remain separate; native edge rendering is retained where metadata exists."},
|
||||
{"pattern":"street_.*", "reason":"Reviewed road/curb family. These are terrain overlays or curb sections, not furniture. No accepted authored replacement with the same section geometry."}
|
||||
]
|
||||
}
|
||||