Skip to content

Commit

Permalink
add trigger_changesky
Browse files Browse the repository at this point in the history
sv_skyname is synced to clients but requires existing clients to rejoin in order to see the new sky. So, a bigass model is drawn instead. The conversion script will generate the needed skybox models for the map.

A BSP is used instead of MDL because world lighting doesn't affect BSPs when used as entity models (no "fullbright" flag for MDL textures in HL), and the entity will render everywhere regardless of VIS. There is less color depth, and the linear texture filter mode is not forced for this kind of sky. The max view distance for the map is forced up to a minimum of 2,097,152 for this effect.

Also, the conversion script now handles bad casing on some worldspawn keys and warns about invalid texture sizes on models (no automatic scaling yet).
  • Loading branch information
wootguy committed Sep 26, 2024
1 parent 06bacbd commit e8987af
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 4 deletions.
86 changes: 83 additions & 3 deletions convert_map.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import struct, os, subprocess, wave, time, sys, copy, codecs, json
import struct, os, subprocess, wave, time, sys, copy, codecs, json, shutil
from collections import OrderedDict

nonstandard_audio_formats = ["aiff", "asf", "asx", "au", "dls", "flac", "fsb", "it", "m3u", "mid", "midi", "mod", "mp2", "ogg", "pls", "s3m", "vag", "wax", "wma", "xm", "xma"]

valve_path = 'C:/Games/Steam/steamapps/common/Half-Life/valve'

def parse_keyvalue(line):
if line.find("//") != -1:
line = line[:line.find("//")]
Expand Down Expand Up @@ -289,6 +291,9 @@ def check_map_problems(all_ents, fix_problems):
global default_files
global converted_files
global modelguy_path
global wadmaker_path
global magick_path
global bspguy_path
global nonstandard_audio_formats

any_problems = False
Expand All @@ -301,6 +306,25 @@ def check_map_problems(all_ents, fix_problems):
scversion = int(ent.get("scversion2", ent.get("scversion", "200")))
if scversion == 4:
scversion = 400

for key in ent.keys():
if key.lower() == 'maxrange' and key != 'MaxRange':
print("MaxRange worldspawn key with bad casing: %s" % key)
if fix_problems:
ent["MaxRange"] = ent[key]
del ent[key]
else:
any_problems = True
break
for key in ent.keys():
if key.lower() == 'waveheight' and key != 'WaveHeight':
print("WaveHeight worldspawn key with bad casing: %s" % key)
if fix_problems:
ent["WaveHeight"] = ent[key]
del ent[key]
else:
any_problems = True
break
break

unique_errors = set()
Expand Down Expand Up @@ -467,6 +491,53 @@ def err(text):
ent['spawnflags'] = "%d" % (spawnflags | 896)
any_problems = True

if cname == "trigger_changesky":
skyname = ent.get('skyname', "")
if not fix_problems:
err("trigger_changesky requires sky to bsp conversion")
any_problems = True
else:
can_convert = True
skybox_image_tmp_path = "_skybox/images"
shutil.rmtree(skybox_image_tmp_path)
os.makedirs(skybox_image_tmp_path, exist_ok=True)
sky_suffixes = ["ft", "bk", "lf", "rt", "up", "dn"]

for suffix in sky_suffixes:
fpath = "gfx/env/%s%s.tga" % (skyname, suffix)
if not os.path.exists(fpath):
fpath = os.path.join(valve_path, fpath)
if not os.path.exists(fpath):
print("ERROR: Failed to find file for trigger_changesky model: %s" % fpath)
can_convert = False
break

cmd = [magick_path, 'convert', '-auto-orient', '-depth', '8', '-type', 'palette', '+dither', fpath, "%s/box_%s.bmp" % (skybox_image_tmp_path, suffix)]
print(' '.join(cmd))
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

if can_convert:
cmd = [wadmaker_path, '-nologfile', '-full', '_skybox/images', 'skybox.wad']
print(' '.join(cmd))
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

cmd = [ripent_path, '-textureimport', '_skybox/skybox']
print(' '.join(cmd))
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

os.makedirs('models/skybox', exist_ok=True)
bsppath = 'models/skybox/%s.bsp' % skyname
shutil.copyfile('_skybox/skybox.bsp', bsppath)

# rename textures because each texture loaded by the client must have a unique name
# without this you can't use 3+ skies or change levels to another map with trigger_changesky
shortskyname = skyname[:13] # leave room for 2 char suffix and the null char
for suffix in sky_suffixes:
newname = suffix + shortskyname # suffix comes first in case the sky name starts with "sky", which is invisible to the client
cmd = [bspguy_path, 'renametex', bsppath, '-old', 'box_%s' % suffix, '-new', newname]
print(' '.join(cmd))
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# custom models with external sequences cause crashes if the "vanilla" model they're referencing
# does not exist (e.g. "models/shocktrooper01.mdl" for a custom shocktrooper model)
for key, value in ent.items():
Expand Down Expand Up @@ -539,6 +610,9 @@ def ents_match(d1, d2, path=""):
cur_dir = os.getcwd()
ripent_path = os.path.join(cur_dir, 'ripent')
modelguy_path = os.path.join(cur_dir, 'modelguy')
wadmaker_path = os.path.join(cur_dir, '_skybox', 'wadmaker')
bspguy_path = os.path.join(cur_dir, 'bspguy')
magick_path = 'magick'

#os.chdir('../compatible_maps')

Expand Down Expand Up @@ -581,6 +655,10 @@ def ents_match(d1, d2, path=""):
with open(json_path, 'r') as file:
data = json.load(file)

for tex in data["textures"]:
if tex["width"] > 512 or tex["height"] > 512:
print("ERROR: Model %s has invalid texture (%s %dx%d)" % (mdl, tex["name"], tex["width"], tex["height"]))

for evt in data["events"]:
for fmt in nonstandard_audio_formats:
if ('.%s' % fmt) in evt['options']:
Expand Down Expand Up @@ -678,7 +756,6 @@ def ents_match(d1, d2, path=""):

print()

print("Converting GSR audio")
for cfg in all_cfgs:
with open("maps/%s" % cfg, 'r') as file:
for line in file:
Expand All @@ -691,7 +768,10 @@ def ents_match(d1, d2, path=""):
mapname = os.path.splitext(cfg)[0]
path = os.path.normpath("sound/%s/%s" % (mapname, parts[1]))
gsr_files.append(path)


if len(gsr_files):
print("Converting GSR audio")

for gsr in gsr_files:
if not os.path.exists(gsr):
print("Missing GSR: %s" % gsr)
Expand Down
1 change: 1 addition & 0 deletions dlls/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ set(TRIGGER_POINT_SRC
triggers/CTriggerCamera.cpp
triggers/CTargetCDAudio.cpp
triggers/CTriggerChangeModel.cpp
triggers/CTriggerChangeSky.cpp
triggers/CTriggerChangeTarget.cpp
triggers/CTriggerChangeValue.cpp
triggers/CTriggerCondition.cpp
Expand Down
101 changes: 101 additions & 0 deletions dlls/triggers/CTriggerChangeSky.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include "extdll.h"
#include "eiface.h"
#include "util.h"
#include "cbase.h"
#include "CBaseDMStart.h"

//
// CTriggerChangeSky / trigger_changesky -- changes the sky textures and lighting at runtime

#define SF_RPLR_REUSABLE 1

#define SKYBOX_MODEL_PATH "models/skybox"

// minimum distance needed to render the fake skybox (bigass model)
#define SKYBOX_MIN_ZMAX 2097152

class CTriggerChangeSky : public CBaseEntity
{
public:
void Spawn(void);
void Precache(void);
void KeyValue(KeyValueData* pkvd);
void Use(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value);

string_t m_skyname;
Vector m_color;
};

LINK_ENTITY_TO_CLASS(trigger_changesky, CTriggerChangeSky)

void CTriggerChangeSky::Spawn(void)
{
Precache();
UTIL_SetOrigin(pev, pev->origin);

pev->movetype = MOVETYPE_NONE;
pev->solid = SOLID_NOT;
pev->effects |= EF_NODRAW;

// using a bsp because it isn't affected by world lighting and renders everywhere
SET_MODEL(ENT(pev), UTIL_VarArgs(SKYBOX_MODEL_PATH "/%s.bsp", STRING(m_skyname)));

edict_t* world = ENT(0);
if (CVAR_GET_FLOAT("sv_zmax") < SKYBOX_MIN_ZMAX) {
ALERT(at_console, "trigger_changesky: increased sv_zmax to %d for skybox rendering\n", SKYBOX_MIN_ZMAX);
CVAR_SET_FLOAT("sv_zmax", SKYBOX_MIN_ZMAX);
}
}

void CTriggerChangeSky::Precache()
{
PRECACHE_MODEL(UTIL_VarArgs(SKYBOX_MODEL_PATH "/%s.bsp", STRING(m_skyname)));
}

void CTriggerChangeSky::KeyValue(KeyValueData* pkvd) {
if (FStrEq(pkvd->szKeyName, "skyname"))
{
m_skyname = ALLOC_STRING(pkvd->szValue);
pkvd->fHandled = TRUE;
}
else if (FStrEq(pkvd->szKeyName, "color"))
{
UTIL_StringToVector(m_color, pkvd->szValue);
pkvd->fHandled = TRUE;
}
else
{
CBaseEntity::KeyValue(pkvd);
}
}

void CTriggerChangeSky::Use(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value)
{
// the engine syncs sky lighting to the client in realtime
CVAR_SET_FLOAT("sv_skycolor_r", m_color.x);
CVAR_SET_FLOAT("sv_skycolor_g", m_color.y);
CVAR_SET_FLOAT("sv_skycolor_b", m_color.z);

// sv_skyname is also synced but clients currently in the server have to rejoin to see it.
// So, a giant model will be drawn in front of the sky instead.
// TODO: You can force the client to unload a sky by sending SVC_RESOURCELIST but
// I don't see any way to force them to load the new one without a full rejoin.
// It is possible to force a rejoin with rehlds but it's not worth it.
//CVAR_SET_STRING("sv_skyname", STRING(m_skyname));

// turn off all skyboxes
CBaseEntity* skyent = NULL;
while (!FNullEnt(skyent = UTIL_FindEntityByClassname(skyent, "trigger_changesky"))) {
skyent->pev->effects |= EF_NODRAW;
}

// ...except this one, unless using the default sky
if (!strcmp(STRING(m_skyname), CVAR_GET_STRING("sv_skyname"))) {
pev->effects |= EF_NODRAW;
ALERT(at_console, "trigger_changesky: activated sky %s (default)\n", STRING(m_skyname));
}
else {
pev->effects = 0;
ALERT(at_console, "trigger_changesky: activated sky %s\n", STRING(m_skyname));
}
}
2 changes: 1 addition & 1 deletion sevenkewp

0 comments on commit e8987af

Please sign in to comment.