Utility for selecting a display model for weapons. It maps a viewmodel path to a corresponding "_display.vmdl" model when present and loadable, caches results per path, and exposes helpers to get or load the chosen model and a console command to report mappings.
using Sandbox;
using System.Collections.Generic;
namespace NZombies;
/// <summary>
/// THE MODEL A GUN IS SHOWN WITH ANYWHERE BUT THE HAND: the wall-buy chalk and its reveal, the box's offer, the
/// Pack-a-Punch, the trade and build tables, third person, the fireworks.
///
/// ⛔ A RENDERER WITH NO CLIP PLAYING DRAWS THE BIND POSE, AND AN MW VIEWMODEL'S BIND IS NOT ITS IDLE. Its parts sit
/// displaced and turned and its spare mag is parked 60+ units off (the WSP Swarm's at 5,144). The hand plays idle, so
/// the gun looks right there and comes apart everywhere else. The user, 2026-10-01: *"the chalk for the weapon models
/// its all broken, even though its perfectly good in my hand"*.
///
/// ⚠️ SO EACH MW GUN SHIPS A DISPLAY MODEL BESIDE ITS VIEWMODEL, `weapons/x/x_display.vmdl` next to
/// `weapons/x/v_x.vmdl`, baked by `Tools/mw_display.py`. It is the idle pose as a still mesh, in the bind's frame
/// (X forward, Z up, the gun body where the bind had it), with the parked props left out. The third-person anchors
/// (`j_gun` …) stay where the bind had them, so every display site's placement is unchanged. A gun without one (every
/// other pack) is shown with its viewmodel, as before.
///
/// ⚠️ CACHED PER PATH, MISSES INCLUDED, AND STATIC: it survives a hotload. A display model added while the game runs
/// shows after a restart.
/// </summary>
public static class WeaponDisplay
{
static readonly Dictionary<string, string> _paths = new();
/// <summary>The path to show <paramref name="viewModelPath"/> with: its display model when it has one that loads, else itself.</summary>
public static string PathFor( string viewModelPath )
{
if ( string.IsNullOrEmpty( viewModelPath ) ) return viewModelPath;
if ( _paths.TryGetValue( viewModelPath, out var hit ) ) return hit;
var result = viewModelPath;
// ⚠️ PLAIN STRING OPS, NOT System.IO.Path, WHICH THE WHITELIST REFUSES (INSTRUCTIONS, SB1000)
var p = viewModelPath.Replace( '\\', '/' );
int slash = p.LastIndexOf( '/' );
var file = p[(slash + 1)..];
if ( file.Length > 7
&& file.StartsWith( "v_", System.StringComparison.OrdinalIgnoreCase )
&& file.EndsWith( ".vmdl", System.StringComparison.OrdinalIgnoreCase ) )
{
var display = $"{p[..(slash + 1)]}{file[2..^5]}_display.vmdl";
// ⚠️ ASKED BEFORE IT IS LOADED: Model.Load of a missing path prints ERROR_FILEOPEN (MysteryBox._models), and
// every gun of every other pack would ask once. A published game has only the compiled `_c`.
if ( FileSystem.Mounted.FileExists( display ) || FileSystem.Mounted.FileExists( display + "_c" ) )
{
// ⚠️ AND ONLY WHEN IT LOADS: Model.Load returns the ERROR model, not null. A display model that did not
// compile falls back to the viewmodel instead of putting a checkerboard on the wall.
var m = Model.Load( display );
if ( m is not null && !m.IsError ) result = display;
else Log.Warning( $"[weapon-display] {display} is there but does not load: showing {file}" );
}
}
_paths[viewModelPath] = result;
return result;
}
/// <summary>The model to show: <paramref name="viewModel"/>'s display model when it has one, else itself.</summary>
public static Model For( Model viewModel )
{
var path = viewModel?.ResourcePath;
if ( string.IsNullOrEmpty( path ) ) return viewModel;
var shown = PathFor( path );
return shown == path ? viewModel : Model.Load( shown );
}
/// <summary>`Model.Load`, preferring the path's display model. The error model for a bad path, as Model.Load gives.</summary>
public static Model Load( string viewModelPath ) => Model.Load( PathFor( viewModelPath ) );
/// <summary>`nz_weapon_display [path]`: what a viewmodel path is shown with, or how many display models are in use.</summary>
[ConCmd( "nz_weapon_display" )]
public static void Report( string path = "" )
{
if ( !string.IsNullOrEmpty( path ) )
{
Log.Info( $"[weapon-display] {path} -> {PathFor( path )}" );
return;
}
int shown = 0;
foreach ( var kv in _paths ) if ( kv.Value != kv.Key ) shown++;
Log.Info( $"[weapon-display] {_paths.Count} viewmodel path(s) asked, {shown} shown with a display model" );
foreach ( var kv in _paths )
if ( kv.Value != kv.Key ) Log.Info( $" {kv.Key} -> {kv.Value}" );
}
}