mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-03-22 14:55:36 +08:00
422 lines
19 KiB
Python
422 lines
19 KiB
Python
#!/usr/bin/python3
|
|
|
|
"""
|
|
Converts CSL packages to the original XSB format for use in LiveTraffic (and probably XSquawkBox)
|
|
For usage info call
|
|
python3 CSL2XSB.py -h
|
|
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2019 B.Hoppe
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
"""
|
|
|
|
from pathlib import Path, PurePath
|
|
import argparse # handling command line arguments
|
|
|
|
_currAircraft = '' # name of currently processed aircraft
|
|
_warnings = 0 # number of warnings
|
|
|
|
# dataRef replacement table: Very simple:
|
|
# if finding the string on the left-hand side (non-libxplanemp dataRef)
|
|
# then replace it with the string on the right-hand side (libxplanemp dataRef)
|
|
_DR = {
|
|
'cjs/world_traffic/engine_rotation_angle1': 'libxplanemp/engines/engine_rotation_angle_deg1',
|
|
'cjs/world_traffic/engine_rotation_angle2': 'libxplanemp/engines/engine_rotation_angle_deg2',
|
|
'cjs/world_traffic/engine_rotation_angle3': 'libxplanemp/engines/engine_rotation_angle_deg3',
|
|
'cjs/world_traffic/engine_rotation_angle4': 'libxplanemp/engines/engine_rotation_angle_deg4',
|
|
'cjs/world_traffic/engine_rpm1': 'libxplanemp/engines/engine_rotation_speed_rpm1',
|
|
'cjs/world_traffic/engine_rpm2': 'libxplanemp/engines/engine_rotation_speed_rpm2',
|
|
'cjs/world_traffic/engine_rpm3': 'libxplanemp/engines/engine_rotation_speed_rpm3',
|
|
'cjs/world_traffic/engine_rpm4': 'libxplanemp/engines/engine_rotation_speed_rpm4',
|
|
'cjs/world_traffic/engine_rad_per_sec1': 'libxplanemp/engines/engine_rotation_speed_rad_sec1',
|
|
'cjs/world_traffic/engine_rad_per_sec2': 'libxplanemp/engines/engine_rotation_speed_rad_sec2',
|
|
'cjs/world_traffic/engine_rad_per_sec3': 'libxplanemp/engines/engine_rotation_speed_rad_sec3',
|
|
'cjs/world_traffic/engine_rad_per_sec4': 'libxplanemp/engines/engine_rotation_speed_rad_sec4',
|
|
'cjs/world_traffic/thrust_reverser_position': 'libxplanemp/engines/thrust_reverser_deploy_ratio',
|
|
'cjs/world_traffic/touch_down': 'libxplanemp/misc/touch_down',
|
|
'cjs/world_traffic/main_gear_deflection': 'libxplanemp/gear/tire_vertical_deflection_mtr',
|
|
'cjs/world_traffic/main_gear_wheel_angle': 'libxplanemp/gear/tire_rotation_angle_deg',
|
|
'cjs/world_traffic/nose_gear_deflection': 'libxplanemp/gear/nose_gear_deflection_mtr',
|
|
'cjs/world_traffic/nose_gear_wheel_angle': 'libxplanemp/gear/tire_rotation_angle_deg',
|
|
'cjs/world_traffic/nose_gear_steering_angle': 'libxplanemp/controls/nws_ratio',
|
|
'cjs/wolrd_traffic/landing_lights_on': 'libxplanemp/controls/landing_lites_on',
|
|
}
|
|
|
|
def OBJ8ReplaceDataRefs(line:str) -> str:
|
|
"""Replaces dataRefs
|
|
|
|
1. Replaces by replacement table, e.g. to replace world_traffic dataRefs
|
|
with libxplanemp/PE dataRefs
|
|
2. If commanded, replaces root 'libxplanemp' by 'LT' (or whatever has been specified)
|
|
"""
|
|
|
|
global _warnings
|
|
|
|
# replace dataRefs as per replacement table
|
|
for old, new in _DR.items():
|
|
line = line.replace(old, new)
|
|
|
|
# if requested replace libxplanemp with something else
|
|
if args.replaceDR is not None:
|
|
line = line.replace('libxplanemp/', args.replaceDR + '/')
|
|
|
|
return line
|
|
|
|
|
|
def UpdateOBJ8File(in_p:Path, out_p:Path, textureLivery:str = None, textureLit:str = None):
|
|
"""Updates the OBJ8 file: TEXTURE and dataRefs."""
|
|
|
|
global _warnings
|
|
|
|
if args.verbose:
|
|
print (' -- Writing ', out_p.name, ' (from ' + in_p.name + '):')
|
|
|
|
assert(in_p.is_file())
|
|
in_f = None
|
|
out_f = None
|
|
try:
|
|
# open in/out files
|
|
in_f = in_p.open(mode='r',encoding='ascii',errors='replace')
|
|
out_f = out_p.open(mode='w',encoding='ascii',errors='replace')
|
|
|
|
# read all lines, copy most of them 1:1 to the output
|
|
for line in in_f:
|
|
# remove the newline char from the end
|
|
line = line.rstrip('\n\r')
|
|
origLine = line
|
|
word = line.split()
|
|
numWords = len(word)
|
|
|
|
if numWords >= 1:
|
|
# replace texture
|
|
if textureLivery is not None and word[0] == 'TEXTURE':
|
|
line = 'TEXTURE ' + textureLivery
|
|
|
|
# replace LIT texture
|
|
if textureLit is not None and word[0] == 'TEXTURE_LIT':
|
|
line = 'TEXTURE_LIT ' + textureLit
|
|
|
|
# dataRefs replacements (only if there is a chance for dataRefs in the line)
|
|
if '/' in line:
|
|
line = OBJ8ReplaceDataRefs(line)
|
|
|
|
# write to output
|
|
if args.verbose and line != origLine:
|
|
print (' Written: ' + line + ' (instead of ' + origLine + ')')
|
|
out_f.write(line+'\n')
|
|
|
|
except IOError as e:
|
|
parser.error('UpdateOBJ8File failed:\n' + e.filename + ':\n'+ e.strerror +'\n')
|
|
|
|
finally:
|
|
# cleanup
|
|
if in_f is not None:
|
|
in_f.close()
|
|
if out_f is not None:
|
|
out_f.close()
|
|
|
|
|
|
def HandleXsbObj8Solid(path: Path, line: str) -> str:
|
|
"""Deals with the OBJ8 SOLID line.
|
|
|
|
Example: OBJ8 SOLID YES MD80/MD80.obj AZA.png MD80_LIT.png
|
|
Means:
|
|
- Identify that it has additional texture info and, hence, requires treatment
|
|
- Have the original OBJ8 file (MD80.obj) treated and MD80_AZA.obj created
|
|
Returns the line for the new xsb_aircraft file, which would be:
|
|
BJ8 SOLID YES MD80/MD80_AZA.obj
|
|
or None if no change occurred.
|
|
"""
|
|
|
|
global _warnings
|
|
|
|
# --- split the line into its parameters (after removing any line ending) ---
|
|
word = line.split()
|
|
numWords = len(word)
|
|
if numWords < 4: # too few parameters!
|
|
print (' ERROR - Too few parameters, skipped: ' + line)
|
|
return None
|
|
|
|
# the object file's name (replace colons [X-CSL comes this way...?] with forward slash)
|
|
object_in_p = PurePath(word[3].replace(':','/'))
|
|
package_name = object_in_p.parts[0]
|
|
# the first part of the Object path just refers to the arbitrary CSL EXPORT_NAME,
|
|
# strip it to get the file name of the current OBJ8 file:
|
|
obj8_in_file_p = path / object_in_p.relative_to(package_name)
|
|
|
|
# --- No additional parameters for livery textures?
|
|
# Example: OBJ8 SOLID YES MD80/MD80.obj
|
|
if numWords == 4:
|
|
if args.noupdate: # and no update of OBJ8?
|
|
if args.verbose:
|
|
print (' No change to: ' + line)
|
|
return None # return with no change
|
|
|
|
# so we rename the existing file to ...orig
|
|
obj8_out_file_p = obj8_in_file_p
|
|
obj8_in_file_p = obj8_in_file_p.with_name(obj8_in_file_p.name + '.orig')
|
|
if not obj8_in_file_p.is_file(): # ...if it isn't there already
|
|
if not obj8_out_file_p.is_file():
|
|
print (' ERROR - ' + _currAircraft + ': Cannot access OBJ8 file, skipped: ' + str(obj8_out_file_p))
|
|
return None
|
|
obj8_out_file_p.rename(obj8_in_file_p)
|
|
if args.verbose:
|
|
print (' Renamed', obj8_out_file_p,'-->',obj8_in_file_p.name)
|
|
|
|
# Update the OBJ8 file
|
|
UpdateOBJ8File(obj8_in_file_p, obj8_out_file_p)
|
|
|
|
# but no change to the xsb_aircraft line
|
|
return line
|
|
|
|
# --- Normal case: additional texture parameters on the line,
|
|
# so we need to create a new OBJ8 file
|
|
else:
|
|
# original OBJ8 file must be accessible
|
|
if not obj8_in_file_p.is_file():
|
|
print (' ERROR - ' + _currAircraft + ': Cannot access OBJ8 file, skipped: ' + str(obj8_in_file_p))
|
|
return None
|
|
|
|
# livery texture and Lit texture (relative to the in-obj8-file)
|
|
textureLivery_p = obj8_in_file_p.parent / word[4]
|
|
if not textureLivery_p.is_file():
|
|
if textureLivery_p.with_suffix('.png').is_file(): # X-CSL sometimes has wrong suffix in xsb_aircraft.txt, i.e. couldn't have worked, fix it, too.
|
|
print (' WARNING - {}: Could not find texture file {}, but found and used {}'.format(_currAircraft, textureLivery_p, textureLivery_p.with_suffix('.png').name))
|
|
textureLivery_p = textureLivery_p.with_suffix('.png')
|
|
if textureLivery_p.with_suffix('.dds').is_file():
|
|
print (' WARNING - {}: Could not find texture file {}, but found and used {}'.format(_currAircraft, textureLivery_p, textureLivery_p.with_suffix('.dds').name))
|
|
textureLivery_p = textureLivery_p.with_suffix('.dds')
|
|
else:
|
|
print (' WARNING - '+_currAircraft+': Cannot find texture file, continue anyway: ', textureLivery_p)
|
|
_warnings += 1
|
|
|
|
# also Lit texture defined? (relative to the in-obj8-file)
|
|
textureLit_p = None
|
|
if numWords >= 6:
|
|
textureLit_p = obj8_in_file_p.parent / word[5]
|
|
if not textureLit_p.is_file():
|
|
if textureLit_p.with_suffix('.png').is_file():
|
|
print (' WARNING - {}: Could not find lit texture file {}, but found and used {}'.format(_currAircraft, textureLit_p, textureLit_p.with_suffix('.png').name))
|
|
textureLit_p = textureLit_p.with_suffix('.png')
|
|
elif textureLit_p.with_suffix('.dds').is_file():
|
|
print (' WARNING - {}: Could not find lit texture file {}, but found and used {}'.format(_currAircraft, textureLit_p, textureLit_p.with_suffix('.dds').name))
|
|
textureLit_p = textureLit_p.with_suffix('.dds')
|
|
else:
|
|
print (' WARNING - '+_currAircraft+': Cannot find lit texture file, continue anyway: ', textureLit_p)
|
|
_warnings += 1
|
|
|
|
# compile the new object file's name, combined from original and livery files:
|
|
obj8_out_file_p = obj8_in_file_p.with_name(obj8_in_file_p.stem + '_' + textureLivery_p.stem + obj8_in_file_p.suffix)
|
|
object_out_p = PurePath(package_name) / obj8_out_file_p.relative_to(path)
|
|
|
|
# Update the OBJ8 file
|
|
UpdateOBJ8File(obj8_in_file_p, obj8_out_file_p, \
|
|
str(textureLivery_p.relative_to(obj8_out_file_p.parent)), \
|
|
str(textureLit_p.relative_to(obj8_out_file_p.parent)) if textureLit_p is not None else None)
|
|
|
|
# --- return the new line for the xsb_aircraft file ---
|
|
newLn = word[0] + ' ' + word[1] + ' ' + word[2] + ' ' + str(object_out_p)
|
|
return newLn
|
|
|
|
|
|
def ConvFolder(path: Path) -> int:
|
|
"""Converts the CSL package in the given path, recursively for each folder.
|
|
|
|
Returns the number of written OBJ8 objects."""
|
|
|
|
numObj = 0;
|
|
global _currAircraft
|
|
_currAircraft = '?'
|
|
commentingOut = 0 # Currently commenting out (due to outdated OBJECT format)?
|
|
|
|
# --- Save the current version of xsb_aircraft as .orig, which we then read from ---
|
|
assert (path.is_dir())
|
|
xsb_aircraft_p = path / 'xsb_aircraft.txt'
|
|
xsb_aircraft_orig_p = path / 'xsb_aircraft.txt.orig'
|
|
|
|
# First check of any of the two exists, otherwise we consider this folder empty:
|
|
if (not xsb_aircraft_p.is_file() and not xsb_aircraft_orig_p.is_file()):
|
|
if args.verbose:
|
|
print ('===(skipped)', path)
|
|
|
|
# --- Recursion: We check for containing folders and try again there!
|
|
if not args.norecursion:
|
|
for folder in path.iterdir():
|
|
if folder.is_dir():
|
|
numObj += ConvFolder(folder)
|
|
|
|
return numObj;
|
|
|
|
# So we will process this path
|
|
print ('=========> ', path)
|
|
|
|
try:
|
|
# If the .orig version _not_ already exists then assume current xsb_aircraft.txt _is_ the original and rename it
|
|
# (Never change the .orig file!)
|
|
if not xsb_aircraft_orig_p.exists():
|
|
xsb_aircraft_p.replace(xsb_aircraft_orig_p)
|
|
if args.verbose:
|
|
print ('Renamed',xsb_aircraft_p,'-->',xsb_aircraft_orig_p)
|
|
|
|
# --- Read from .orig as the original master ---
|
|
xsb_aircraft_orig_f = xsb_aircraft_orig_p.open(mode='r',encoding='ascii',errors='replace')
|
|
if args.verbose:
|
|
print ('Reading from', xsb_aircraft_orig_p)
|
|
|
|
# --- Write to a new xsb_aircraft.txt file (overwrite existing one!) ---
|
|
xsb_aircraft_f = xsb_aircraft_p.open(mode='w',encoding='ascii',errors='replace')
|
|
if args.verbose:
|
|
print ('Writing to ', xsb_aircraft_p)
|
|
|
|
# --- Read all lines from .orig file
|
|
# Most of them are just copied 1:1 to the output file
|
|
for line in xsb_aircraft_orig_f:
|
|
# remove the newline char from the end
|
|
line = line.rstrip('\n\r')
|
|
origLine = line
|
|
word = line.split()
|
|
numWords = len(word)
|
|
|
|
if numWords >= 2:
|
|
# This is a line with at least two words
|
|
|
|
# OBJECT is an outdated format modern XPMP2 no longer supports
|
|
# Comment out the entire aircraft
|
|
if word[0] == 'OBJECT' or word[0] == 'AIRCRAFT':
|
|
# assume all else is the aircraft name
|
|
_currAircraft = ' '.join(word[1:])
|
|
commentingOut = 1
|
|
print (' WARNING - {}: Outdated format, commented out'.format(_currAircraft))
|
|
line = "--- Outdated format, no longer valid\n# " + line
|
|
|
|
# OBJ8_AIRCRAFT identifies the start of another aircraft.
|
|
# Technically, we don't need that info, but it's nice for user info
|
|
if word[0] == 'OBJ8_AIRCRAFT':
|
|
# assume all else is the aircraft name
|
|
_currAircraft = ' '.join(word[1:])
|
|
if args.verbose:
|
|
print ('-- ' + _currAircraft)
|
|
# replace spaces in the aircraft name (PE does that)
|
|
if ' ' in _currAircraft:
|
|
_currAircraft = _currAircraft.replace(' ', '_')
|
|
# replace colons in the aircraft name (X-CSL does that, although there is no need for a path here)
|
|
if ':' in _currAircraft:
|
|
_currAircraft = _currAircraft.replace(':', '_')
|
|
# re-write the OBJ8_AIRCRAFT line
|
|
word[1] = _currAircraft
|
|
line = 'OBJ8_AIRCRAFT ' + _currAircraft
|
|
# Valid format, shall not be commented out
|
|
commentingOut = 0
|
|
|
|
# X-CSL uses non-existing ICAO code 'MD80', replace with MD81
|
|
if word[1] == 'MD80' and \
|
|
(word[0] == 'ICAO' or \
|
|
word[0] == 'AIRLINE' or \
|
|
word[0] == 'LIVERY'):
|
|
word[1] = 'MD81'
|
|
line = ' '.join(word)
|
|
|
|
# -- now decide what to do with the line
|
|
|
|
# ignore deprecated or PE-extension commands
|
|
if (word[0] == 'OBJ8' and word[1] == 'LOW_LOD') or \
|
|
word[0] == 'HASGEAR' or \
|
|
word[0] == 'TEXTURE':
|
|
line = None
|
|
|
|
# OBJ8 is the one line we _really_ need to work on!
|
|
elif (word[0] == 'OBJ8'):
|
|
Obj8SolidLine = HandleXsbObj8Solid(path, line)
|
|
if Obj8SolidLine is not None:
|
|
# and we did something to the OBJ8 line:
|
|
line = Obj8SolidLine
|
|
numObj += 1
|
|
|
|
# -- write the resulting line out to the new xsb_aircraft file
|
|
if line is not None:
|
|
if commentingOut:
|
|
xsb_aircraft_f.write("# ")
|
|
|
|
xsb_aircraft_f.write(line + '\n')
|
|
if args.verbose and origLine != line:
|
|
print (' Written: ' + line + ' (instead of: ' + origLine + ')')
|
|
else:
|
|
if args.verbose:
|
|
print (' Removed line: ' + origLine)
|
|
|
|
# --- Done, cleanup
|
|
xsb_aircraft_f.close()
|
|
xsb_aircraft_orig_f.close()
|
|
print (' ', path, 'done, converted', numObj, 'OBJ8 files')
|
|
return numObj
|
|
|
|
except IOError as e:
|
|
parser.error('Converting folder ' + str(path) + ' failed:\n' + e.filename + ':\n'+ e.strerror +'\n')
|
|
|
|
|
|
|
|
|
|
""" === MAIN === """
|
|
# --- Handling command line argumens ---
|
|
parser = argparse.ArgumentParser(description='CSL2XSB 0.3.1: Convert CSL packages to XPMP2 format, convert some animation dataRefs. Tested with: Bluebell, X-CSL.',fromfile_prefix_chars='@')
|
|
parser.add_argument('path', help='Base path, searched recursively for CSL packages identified by existing xsb_aircraft.txt files', nargs='?', default='NULL')
|
|
parser.add_argument('--noupdate', help='Suppress update of OBJ8 files if there are no additional textures', action='store_true')
|
|
parser.add_argument('--norecursion', help='Do not search directories recursively', action='store_true')
|
|
parser.add_argument('-v', '--verbose', help='More detailed output about every change', action='store_true')
|
|
parser.add_argument('--replaceDR', metavar="TEXT", help="Replace dataRef's root 'libxplanemp' with TEXT.\nCAUTION: This works with LiveTraffic up to v1.5 only. CSLs' animations/lights will no longer work with standard multipayer clients nor with LiveTraffic starting from v2.0!")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# If called with no argument (e.g. by double-clicking the script) ask the user interactively
|
|
# if (s)he likes to work on the current directory.
|
|
UserWantsIt = None
|
|
if args.path == 'NULL':
|
|
args.path = Path.cwd()
|
|
print (parser.description)
|
|
print ('Do you want to run CSL2XSB on the current directory "'+str(args.path)+'"?')
|
|
while True:
|
|
UserWantsIt = input ('Answer "y" or "n": ')
|
|
if UserWantsIt.upper() == 'N':
|
|
print ('You answered "N", so we exit without doing anything. Try "python CSL2XSBpy -h" for help.')
|
|
exit()
|
|
if UserWantsIt.upper() == 'Y':
|
|
break
|
|
|
|
# normalize the path, resolves relative paths and makes nice directory delimiters
|
|
basePath = Path(args.path)
|
|
if not basePath.exists() or not basePath.is_dir():
|
|
parser.error('Base bath "' + str(basePath) + '" does not exist or is no directory.')
|
|
|
|
if args.verbose:
|
|
print ('Base path: ', basePath)
|
|
|
|
# --- Do it ---
|
|
numConverted = ConvFolder(basePath)
|
|
print ('Done. Converted ' + str(numConverted) + ' OBJ8 files in total. Produced ' + str(_warnings) + ' warning(s).')
|
|
|
|
# Running interactively?
|
|
if UserWantsIt is not None:
|
|
input ("Hit [Enter] to finish.")
|
|
|
|
# --- Done ---
|
|
exit(0)
|