Skip to content

Instantly share code, notes, and snippets.

@Rainyan
Last active November 25, 2018 05:23
Show Gist options
  • Save Rainyan/43b83ca975d9382a00f520ec091f8d3a to your computer and use it in GitHub Desktop.
Save Rainyan/43b83ca975d9382a00f520ec091f8d3a to your computer and use it in GitHub Desktop.
Python script to generate QC files for Source SDK studiomdl compiler.
#!/usr/bin/env python
"""Generate a QC file for Source SDK studiomdl compile."""
# TODO: 2 separate commands for
# 1. generating qc (and storing to file) without running studiomdl
# 2. running studiomdl with the qc
import os
import subprocess
import warnings
# Link to the latest source code for this script.
SOURCECODE_URL = 'https://gist.github.com/Rainyan/43b83ca975d9382a00f520ec091f8d3a'
SOURCECODE_VERSION = '2018-11-25'
# This is the game root path with GameInfo.txt, materials, models, etc.
GAME_PATH = 'C:/Program Files (x86)/Steam/steamapps/common/NEOTOKYO/mapping'
QC_PATH = os.path.dirname(os.path.realpath(__file__)) + '/buildmodel.qc'
STUDIOMDL_BIN = 'C:/Program Files (x86)/Steam/steamapps/common/SourceSDK/bin/ep1/bin/studiomdl.exe'
# Folder path inside "models"; leave empty to use the root /models/ as location
MODELNAME_PATH = 'custom/snowfall'
# Name of the base model
MODELNAME_BASE = 'tunnel_maincurve' + '.smd'
# Name of the collision model
MODELNAME_COLLISION = 'hacd_tunnel_maincurve' + '.collision.smd'
# SMD path, relative to this script location
STUDIO_MDL_PATH = 'models'
# $cdmaterials path
CD_MATERIALS_PATH = 'models/' + MODELNAME_PATH
# Is Source engine version EP1 being used?
IS_EP1_ENGINE = True
# $staticprop, not necessarily prop_static
IS_STATICPROP = True
# Is collision concave?
IS_CONCAVE = False
# Is collision costly?
IS_COSTLY_COLLISION_MODEL = False
# Is physics prop?
IS_PHYSICAL = False
# Show generated QC in terminal (this can make Python warnings hard to find)?
OUTPUT_GENERATED_QC = False
if IS_COSTLY_COLLISION_MODEL and not IS_EP1_ENGINE:
# For Orange Box and newer, $collisionmodel->$maxconvexpieces sets the max
# collision model pieces for costly collision. This defaults to 20, and should
# be set higher than that (this is sanity checked for in the QC generation).
OB_MAX_CONVEX_PIECES = 20
# https://developer.valvesoftware.com/wiki/Prop_data_base_types
PROP_DATA_BASE_TYPE = 'Stone.Large'
# https://developer.valvesoftware.com/wiki/Material_surface_properties
SURFACE_PROP = 'concrete'
# float, in kg
MASS = 1000.0
# Helpers for output nicifying
TAB_OFFSET = ' '
OPEN_BRACE = '{'
CLOSE_BRACE = '}'
def main():
"""Script entrypoint."""
qc = generate_qc()
write_qc_to_file(qc)
run_studiomdl()
def write_qc_to_file(qc):
"""Iterate through a string list of QC data,
and write it to file."""
print('- Writing QC to "' + QC_PATH + '"...')
if (OUTPUT_GENERATED_QC): print('')
f = open(QC_PATH, 'w')
for line in qc:
if (OUTPUT_GENERATED_QC): print(line)
f.write(line + '\n')
f.close()
if (OUTPUT_GENERATED_QC): print('')
print('- QC file ready.')
def generate_qc():
"""Build a string list of relevant QC data to process."""
print('- Building QC data...')
qc = []
qc.append('// This QC file was auto-generated by a Python script:')
qc.append('// ' + os.path.basename(__file__) + ' version ' + SOURCECODE_VERSION)
qc.append('// Source: ' + SOURCECODE_URL)
qc.append('// Note that any manual edits will get overwritten if the script is run again!')
qc.append('')
# modelname
qc.append('$modelname "' + MODELNAME_PATH + '/' + MODELNAME_BASE + '"')
qc.append('$cdmaterials "' + CD_MATERIALS_PATH + '"')
qc.append('')
# body
qc.append('$body studio "' + STUDIO_MDL_PATH + '/' + MODELNAME_BASE + '"')
if (IS_STATICPROP):
qc.append('$staticprop')
qc.append('$surfaceprop ' + SURFACE_PROP)
qc.append('')
# sequence
qc.append('$sequence idle "' + STUDIO_MDL_PATH + '/' + MODELNAME_BASE + '"')
qc.append('')
# collisionmodel
if IS_COSTLY_COLLISION_MODEL and IS_EP1_ENGINE:
qc.append('// On ep1 engine, run studiomdl with -fullcollide ' +\
'if compiling costly collision models.')
qc.append('// For details, see: ' +\
'https://developer.valvesoftware.com/wiki/Costly_collision_model')
qc.append('$collisionmodel "' + STUDIO_MDL_PATH + '/' + MODELNAME_COLLISION + '"')
qc.append(OPEN_BRACE)
qc.append(TAB_OFFSET + '$mass ' + str(MASS))
if (IS_CONCAVE):
qc.append(TAB_OFFSET + '$concave')
if IS_COSTLY_COLLISION_MODEL and not IS_EP1_ENGINE:
if OB_MAX_CONVEX_PIECES <= 20:
warning = 'Compiling OB costly collision with $maxconvexpieces <= default! ' +\
'This is probably a mistake.'
warnings.warn(warning)
qc.append(TAB_OFFSET + '// WARNING: ' + warning)
qc.append(TAB_OFFSET + '$maxconvexpieces ' + str(OB_MAX_CONVEX_PIECES))
qc.append(CLOSE_BRACE)
# keyvalues
if (IS_PHYSICAL):
qc.append('')
qc.append('$keyvalues')
qc.append(OPEN_BRACE)
qc.append(TAB_OFFSET + 'prop_data')
qc.append(TAB_OFFSET + OPEN_BRACE)
qc.append(TAB_OFFSET + TAB_OFFSET + 'base ' + PROP_DATA_BASE_TYPE)
qc.append(TAB_OFFSET + CLOSE_BRACE)
qc.append(CLOSE_BRACE)
return qc
def list_to_str(list):
"""Take a list and output it as a concatenated
string with whitespace dividers."""
output = ''
for item in list:
output += ' ' + item
output += ' '
return output
def run_studiomdl():
"""Call studiomdl to process the generated QC file,
and compile it into Source engine model data."""
print('- Running studiomdl...')
args = ['-game', GAME_PATH, '-nop4', '-verbose']
if (IS_COSTLY_COLLISION_MODEL):
args.append('-fullcollide')
command = '"' + STUDIOMDL_BIN + '"' + list_to_str(args) + '"' + QC_PATH + '"'
print(command)
subprocess.run(command)
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment