pytol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytol/__init__.py +5 -0
- pytol/parsers/vtm_parser.py +132 -0
- pytol/parsers/vts_builder.py +201 -0
- pytol/resources/city_layouts_database.json +11193 -0
- pytol/resources/guid_to_name.json +89 -0
- pytol/resources/individual_prefabs_database.json +10980 -0
- pytol/resources/noise.png +0 -0
- pytol/resources/resources.py +55 -0
- pytol/resources/vehicle_equip_database.json +293 -0
- pytol/terrain/mission_terrain_helper.py +1989 -0
- pytol/terrain/terrain_calculator.py +537 -0
- pytol-0.1.0.dist-info/METADATA +20 -0
- pytol-0.1.0.dist-info/RECORD +15 -0
- pytol-0.1.0.dist-info/WHEEL +4 -0
- pytol-0.1.0.dist-info/licenses/LICENSE +674 -0
pytol/__init__.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pprint
|
|
2
|
+
|
|
3
|
+
def _parse_value(val_str):
|
|
4
|
+
"""Helper function to convert a string value to an appropriate type."""
|
|
5
|
+
val_str = val_str.strip()
|
|
6
|
+
|
|
7
|
+
# Handle empty values
|
|
8
|
+
if not val_str:
|
|
9
|
+
return ""
|
|
10
|
+
|
|
11
|
+
# Boolean
|
|
12
|
+
if val_str.lower() == 'true':
|
|
13
|
+
return True
|
|
14
|
+
if val_str.lower() == 'false':
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
# Vector/Tuple like (x, y, z)
|
|
18
|
+
if val_str.startswith('(') and val_str.endswith(')'):
|
|
19
|
+
try:
|
|
20
|
+
# Remove parentheses and split by comma, then convert each part to float
|
|
21
|
+
return tuple(float(x) for x in val_str[1:-1].split(','))
|
|
22
|
+
except (ValueError, TypeError):
|
|
23
|
+
return val_str # Return as string if conversion fails
|
|
24
|
+
|
|
25
|
+
# List separated by semicolons
|
|
26
|
+
if ';' in val_str:
|
|
27
|
+
# Return a list of non-empty items
|
|
28
|
+
return [item for item in val_str.split(';') if item]
|
|
29
|
+
|
|
30
|
+
# Number (float or int)
|
|
31
|
+
try:
|
|
32
|
+
# Check for scientific notation or decimal to decide float vs int
|
|
33
|
+
if '.' in val_str or 'e' in val_str.lower():
|
|
34
|
+
return float(val_str)
|
|
35
|
+
return int(val_str)
|
|
36
|
+
except ValueError:
|
|
37
|
+
# If all else fails, it's a string
|
|
38
|
+
return val_str
|
|
39
|
+
|
|
40
|
+
def _parse_block(lines_iterator):
|
|
41
|
+
"""Recursively parses a block of text enclosed in curly braces."""
|
|
42
|
+
data_dict = {}
|
|
43
|
+
|
|
44
|
+
for line in lines_iterator:
|
|
45
|
+
line = line.strip()
|
|
46
|
+
|
|
47
|
+
if not line:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# End of the current block
|
|
51
|
+
if line == '}':
|
|
52
|
+
return data_dict
|
|
53
|
+
|
|
54
|
+
# Key-value pair
|
|
55
|
+
if '=' in line:
|
|
56
|
+
key, value = line.split('=', 1)
|
|
57
|
+
key = key.strip()
|
|
58
|
+
data_dict[key] = _parse_value(value)
|
|
59
|
+
# A new block is starting
|
|
60
|
+
else:
|
|
61
|
+
key = line.strip()
|
|
62
|
+
# The next line should be '{', so we skip it.
|
|
63
|
+
try:
|
|
64
|
+
if next(lines_iterator).strip() != '{':
|
|
65
|
+
# Handle cases where a block might be empty or formatted differently
|
|
66
|
+
continue
|
|
67
|
+
except StopIteration:
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
block_content = _parse_block(lines_iterator)
|
|
71
|
+
|
|
72
|
+
# If a key (like 'Segment' or 'Chunk') appears multiple times, store as a list
|
|
73
|
+
if key in data_dict:
|
|
74
|
+
if not isinstance(data_dict[key], list):
|
|
75
|
+
data_dict[key] = [data_dict[key]] # Convert existing item to a list
|
|
76
|
+
data_dict[key].append(block_content)
|
|
77
|
+
else:
|
|
78
|
+
data_dict[key] = block_content
|
|
79
|
+
|
|
80
|
+
return data_dict
|
|
81
|
+
|
|
82
|
+
def parse_vtol_data(file_content):
|
|
83
|
+
"""
|
|
84
|
+
Parses the full text of a VTOL VR map (.vtm) or mission (.vts) file.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file_content (str): The string content of the file.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
dict: A nested dictionary representing the file's structure.
|
|
91
|
+
"""
|
|
92
|
+
lines = iter(file_content.strip().split('\n'))
|
|
93
|
+
|
|
94
|
+
# The first line is the root object name (e.g., 'VTMapCustom')
|
|
95
|
+
root_key = next(lines).strip()
|
|
96
|
+
|
|
97
|
+
# The second line is the opening brace '{'
|
|
98
|
+
next(lines)
|
|
99
|
+
|
|
100
|
+
# Start parsing the main block
|
|
101
|
+
return {root_key: _parse_block(lines)}
|
|
102
|
+
|
|
103
|
+
if __name__ == '__main__':
|
|
104
|
+
file_path = "test_map/hMap2/hMap2.vtm"
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
with open(file_path, "r") as f:
|
|
108
|
+
vtm_content = f.read()
|
|
109
|
+
|
|
110
|
+
print(f"Successfully read '{file_path}'. Parsing now...")
|
|
111
|
+
|
|
112
|
+
# Call the parser function
|
|
113
|
+
parsed_map_data = parse_vtol_data(vtm_content)
|
|
114
|
+
|
|
115
|
+
print("\n--- Parse Complete! ---")
|
|
116
|
+
|
|
117
|
+
# Example: Accessing specific data from the parsed map
|
|
118
|
+
map_id = parsed_map_data.get("VTMapCustom", {}).get("mapID")
|
|
119
|
+
max_height = parsed_map_data.get("VTMapCustom", {}).get("TerrainSettings", {}).get("maxMtnHeight")
|
|
120
|
+
|
|
121
|
+
print(f"Map ID: {map_id}")
|
|
122
|
+
print(f"Max Mountain Height: {max_height} meters")
|
|
123
|
+
|
|
124
|
+
# Using pprint to cleanly print a small, complex section (like the first prefab)
|
|
125
|
+
first_prefab = parsed_map_data.get("VTMapCustom", {}).get("StaticPrefabs", {}).get("StaticPrefab", [{}])[0]
|
|
126
|
+
print("\n--- Data for the first StaticPrefab: ---")
|
|
127
|
+
pprint.pprint(first_prefab)
|
|
128
|
+
|
|
129
|
+
except FileNotFoundError:
|
|
130
|
+
print(f"Error: The file '{file_path}' was not found.")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"An error occurred during parsing: {e}")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# mission_builder.py
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
class EventTarget:
|
|
6
|
+
def __init__(self, target_type, target_id, event_name, params=None):
|
|
7
|
+
self.target_type = target_type
|
|
8
|
+
self.target_id = target_id
|
|
9
|
+
self.event_name = event_name
|
|
10
|
+
self.params = params or []
|
|
11
|
+
|
|
12
|
+
class ParamInfo:
|
|
13
|
+
def __init__(self, name, param_type, value):
|
|
14
|
+
self.name = name
|
|
15
|
+
self.type = param_type
|
|
16
|
+
self.value = value
|
|
17
|
+
|
|
18
|
+
def _format_value(val):
|
|
19
|
+
if isinstance(val, bool):
|
|
20
|
+
return str(val)
|
|
21
|
+
if isinstance(val, str):
|
|
22
|
+
return val
|
|
23
|
+
if isinstance(val, (int, float)):
|
|
24
|
+
return str(val)
|
|
25
|
+
return str(val)
|
|
26
|
+
|
|
27
|
+
def _format_vector(vec):
|
|
28
|
+
return f"({vec[0]:.6f}, {vec[1]:.6f}, {vec[2]:.6f})"
|
|
29
|
+
|
|
30
|
+
def _format_point_list(points):
|
|
31
|
+
return ";".join([_format_vector(p) for p in points])
|
|
32
|
+
|
|
33
|
+
def _format_id_list(ids):
|
|
34
|
+
return ";".join(map(str, ids))
|
|
35
|
+
|
|
36
|
+
def _format_block(name, content_str, indent_level=1):
|
|
37
|
+
indent = "\t" * indent_level
|
|
38
|
+
if not content_str.strip():
|
|
39
|
+
return f"{indent}{name}\n{indent}{{\n{indent}}}\n"
|
|
40
|
+
return f"{indent}{name}\n{indent}{{\n{content_str}{indent}}}\n"
|
|
41
|
+
|
|
42
|
+
class Mission:
|
|
43
|
+
def __init__(self, scenario_name, scenario_id, description, vehicle="F/A-26B", map_id: str = "", map_path: str = "", vtol_directory: str = ''):
|
|
44
|
+
self.scenario_name = scenario_name
|
|
45
|
+
self.scenario_id = scenario_id
|
|
46
|
+
|
|
47
|
+
if map_path:
|
|
48
|
+
self.map_path = map_path
|
|
49
|
+
self.map_id = os.path.basename(map_path)
|
|
50
|
+
elif map_id and os.getenv('VTOL_VR_DIR'):
|
|
51
|
+
self.map_path = os.path.join(os.getenv('VTOL_VR_DIR'), "CustomMaps", map_id)
|
|
52
|
+
self.map_id = map_id
|
|
53
|
+
elif map_id and vtol_directory:
|
|
54
|
+
self.map_path = os.path.join(vtol_directory, "CustomMaps", map_id)
|
|
55
|
+
self.map_id = map_id
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError("Either map_id or map_path must be provided, and VTOL_VR_DIR must be set if using only map_id.")
|
|
58
|
+
|
|
59
|
+
self.scenario_description = description
|
|
60
|
+
self.vehicle = vehicle
|
|
61
|
+
|
|
62
|
+
self.units, self.paths, self.waypoints, self.trigger_events, self.objectives, self.static_objects, self.bases, self.briefing_notes = [], [], [], [], [], [], [], []
|
|
63
|
+
self.unit_groups = {"ALLIED": {}, "ENEMY": {}}
|
|
64
|
+
self.timed_event_groups = []
|
|
65
|
+
self.resource_manifest = {}
|
|
66
|
+
self.conditionals_raw = "" # Para pegar código de Conditionals directamente
|
|
67
|
+
|
|
68
|
+
self.allowed_equips = "fa26_droptank;fa26_gun;"
|
|
69
|
+
self.rtb_wpt_id = ""
|
|
70
|
+
self.refuel_wpt_id = ""
|
|
71
|
+
|
|
72
|
+
def add_unit(self, unit_id, unit_name, global_position, rotation, unit_fields=None):
|
|
73
|
+
uid = len(self.units)
|
|
74
|
+
self.units.append({'unitID': unit_id, 'unitName': unit_name, 'globalPosition': global_position, 'rotation': rotation, 'unitInstanceID': uid, 'unit_fields': unit_fields or {}})
|
|
75
|
+
print(f"Unidad '{unit_name}' añadida (ID de Instancia: {uid})")
|
|
76
|
+
return uid
|
|
77
|
+
|
|
78
|
+
def add_path(self, path_id, name, points, loop=False, path_mode="Smooth"):
|
|
79
|
+
self.paths.append({'id': path_id, 'name': name, 'points': points, 'loop': loop, 'pathMode': path_mode})
|
|
80
|
+
|
|
81
|
+
def add_waypoint(self, waypoint_id, name, global_point):
|
|
82
|
+
self.waypoints.append({'id': waypoint_id, 'name': name, 'globalPoint': global_point})
|
|
83
|
+
|
|
84
|
+
def add_unit_to_group(self, team, group_name, unit_instance_id):
|
|
85
|
+
team_upper = team.upper()
|
|
86
|
+
if team_upper not in self.unit_groups:
|
|
87
|
+
return
|
|
88
|
+
if group_name not in self.unit_groups[team_upper]:
|
|
89
|
+
self.unit_groups[team_upper][group_name] = []
|
|
90
|
+
self.unit_groups[team_upper][group_name].append(unit_instance_id)
|
|
91
|
+
|
|
92
|
+
def add_objective(self, objective_id, name, info, obj_type, fields, required=True, waypoint="", prereqs=None, auto_set_waypoint=True):
|
|
93
|
+
self.objectives.append({'id': objective_id, 'name': name, 'info': info, 'type': obj_type, 'fields': fields, 'required': required, 'waypoint': waypoint, 'prereqs': prereqs or [], 'auto_set_waypoint': auto_set_waypoint})
|
|
94
|
+
|
|
95
|
+
def add_static_object(self, prefab_id, global_pos, rotation):
|
|
96
|
+
sid = len(self.static_objects)
|
|
97
|
+
self.static_objects.append({'id': sid, 'prefabID': prefab_id, 'globalPos': global_pos, 'rotation': rotation})
|
|
98
|
+
return sid
|
|
99
|
+
|
|
100
|
+
def add_trigger_event(self, event_id, name, trigger_type, event_targets, enabled=True, **kwargs):
|
|
101
|
+
trigger = {'id': event_id, 'name': name, 'type': trigger_type, 'targets': event_targets, 'enabled': enabled, 'props': kwargs}
|
|
102
|
+
self.trigger_events.append(trigger)
|
|
103
|
+
|
|
104
|
+
def add_base(self, base_id, team, name=""): self.bases.append({'id': base_id, 'team': team, 'name': name})
|
|
105
|
+
def add_briefing_note(self, text): self.briefing_notes.append({'text': text})
|
|
106
|
+
def add_resource(self, res_id, path): self.resource_manifest[res_id] = path
|
|
107
|
+
def set_conditionals_raw(self, raw_string): self.conditionals_raw = raw_string
|
|
108
|
+
|
|
109
|
+
def _generate_content_string(self):
|
|
110
|
+
"""Inner generation of the VTS content string."""
|
|
111
|
+
# Units
|
|
112
|
+
units_c = ""
|
|
113
|
+
for u in self.units:
|
|
114
|
+
fields_c = "".join([f"\t\t\t\t{k} = {_format_value(v)}\n" for k,v in u['unit_fields'].items()])
|
|
115
|
+
units_c += f"\t\tUnitSpawner\n\t\t{{\n" \
|
|
116
|
+
f"\t\t\tunitName = {u['unitName']}\n" \
|
|
117
|
+
f"\t\t\tglobalPosition = {_format_vector(u['globalPosition'])}\n" \
|
|
118
|
+
f"\t\t\tunitInstanceID = {u['unitInstanceID']}\n" \
|
|
119
|
+
f"\t\t\tunitID = {u['unitID']}\n" \
|
|
120
|
+
f"\t\t\trotation = {_format_vector(u['rotation'])}\n" \
|
|
121
|
+
f"{_format_block('UnitFields', fields_c, 3)}\t\t}}\n"
|
|
122
|
+
|
|
123
|
+
# Paths y Waypoints
|
|
124
|
+
paths_c = "".join([f"\t\tPATH\n\t\t{{\n\t\t\tid = {p['id']}\n\t\t\tname = {p['name']}\n\t\t\tloop = {p['loop']}\n\t\t\tpoints = {_format_point_list(p['points'])}\n\t\t\tpathMode = {p['pathMode']}\n\t\t}}\n" for p in self.paths])
|
|
125
|
+
wpts_c = "".join([f"\t\tWAYPOINT\n\t\t{{\n\t\t\tid = {w['id']}\n\t\t\tname = {w['name']}\n\t\t\tglobalPoint = {_format_vector(w['globalPoint'])}\n\t\t}}\n" for w in self.waypoints])
|
|
126
|
+
|
|
127
|
+
# Unit Groups
|
|
128
|
+
ug_c = ""
|
|
129
|
+
for team, groups in self.unit_groups.items():
|
|
130
|
+
team_c = "".join([f"\t\t\t{name} = 2;{_format_id_list(ids)};\n" for name, ids in groups.items()])
|
|
131
|
+
ug_c += _format_block(team, team_c, 2)
|
|
132
|
+
|
|
133
|
+
# Trigger Events
|
|
134
|
+
triggers_c = ""
|
|
135
|
+
for t in self.trigger_events:
|
|
136
|
+
props_c = "".join([f"\t\t\t{k} = {v}\n" for k, v in t['props'].items()])
|
|
137
|
+
targets_c = ""
|
|
138
|
+
for target in t['targets']:
|
|
139
|
+
params_c = "".join([f"\t\t\t\t\tParamInfo\n\t\t\t\t\t{{\n\t\t\t\t\t\ttype = {p.type}\n\t\t\t\t\t\tvalue = {p.value}\n\t\t\t\t\t\tname = {p.name}\n\t\t\t\t\t}}\n" for p in target.params])
|
|
140
|
+
targets_c += f"\t\t\t\tEventTarget\n\t\t\t\t{{\n\t\t\t\t\ttargetType = {target.target_type}\n\t\t\t\t\ttargetID = {target.target_id}\n\t\t\t\t\teventName = {target.event_name}\n{params_c}\t\t\t\t}}\n"
|
|
141
|
+
event_info = _format_block('EventInfo', f"\t\t\t\teventName = \n{targets_c}", 3)
|
|
142
|
+
triggers_c += f"\t\tTriggerEvent\n\t\t{{\n\t\t\tid = {t['id']}\n\t\t\tenabled = {t['enabled']}\n\t\t\ttriggerType = {t['type']}\n{props_c}{event_info}\t\t}}\n"
|
|
143
|
+
|
|
144
|
+
# Objectives
|
|
145
|
+
objs_c = "".join([f"\t\tObjective\n\t\t{{\n\t\t\tobjectiveName = {o['name']}\n\t\t\tobjectiveInfo = {o['info']}\n\t\t\tobjectiveID = {o['id']}\n\t\t\trequired = {o['required']}\n\t\t\twaypoint = {o['waypoint']}\n\t\t\tautoSetWaypoint = {o['auto_set_waypoint']}\n\t\t\tstartMode = {'PreReqs' if o['prereqs'] else 'Immediate'}\n\t\t\tobjectiveType = {o['type']}\n\t\t\tpreReqObjectives = {_format_id_list(o['prereqs'])};\n{_format_block('fields', ''.join([f'\\t\\t\\t\\t{k} = {_format_value(v)}\\n' for k,v in o['fields'].items()]), 3)}\t\t}}\n" for o in self.objectives])
|
|
146
|
+
|
|
147
|
+
# Static Objects
|
|
148
|
+
statics_c = "".join([f"\t\tStaticObject\n\t\t{{\n\t\t\tprefabID = {s['prefabID']}\n\t\t\tid = {s['id']}\n\t\t\tglobalPos = {_format_vector(s['globalPos'])}\n\t\t\trotation = {_format_vector(s['rotation'])}\n\t\t}}\n" for s in self.static_objects])
|
|
149
|
+
|
|
150
|
+
# Bases y Briefing
|
|
151
|
+
bases_c = "".join([f"\t\tBaseInfo\n\t\t{{\n\t\t\tid = {b['id']}\n\t\t\toverrideBaseName = {b['name']}\n\t\t\tbaseTeam = {b['team']}\n\t\t}}\n" for b in self.bases])
|
|
152
|
+
briefing_c = "".join([f"\t\tBRIEFING_NOTE\n\t\t{{\n\t\t\ttext = {n['text']}\n\t\t\timagePath = \n\t\t\taudioClipPath = \n\t\t}}\n" for n in self.briefing_notes])
|
|
153
|
+
|
|
154
|
+
# Resource Manifest
|
|
155
|
+
resources_c = "".join([f"\t\t{k} = {v}\n" for k, v in self.resource_manifest.items()])
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"UNITS": units_c, "PATHS": paths_c, "WAYPOINTS": wpts_c, "UNITGROUPS": ug_c,
|
|
159
|
+
"TRIGGER_EVENTS": triggers_c, "OBJECTIVES": objs_c, "StaticObjects": statics_c,
|
|
160
|
+
"BASES": bases_c, "Briefing": briefing_c, "ResourceManifest": resources_c
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def _save_to_file(self, path):
|
|
164
|
+
c = self._generate_content_string()
|
|
165
|
+
vts = "CustomScenario\n{\n"
|
|
166
|
+
vts += f"\tscenarioName = {self.scenario_name}\n"
|
|
167
|
+
vts += f"\tscenarioID = {self.scenario_id}\n"
|
|
168
|
+
vts += f"\tscenarioDescription = {self.scenario_description}\n"
|
|
169
|
+
vts += f"\tmapID = {self.map_id}\n"
|
|
170
|
+
vts += f"\tvehicle = {self.vehicle}\n"
|
|
171
|
+
vts += f"\trtbWptID = {self.rtb_wpt_id}\n"
|
|
172
|
+
vts += f"\trefuelWptID = {self.refuel_wpt_id}\n"
|
|
173
|
+
|
|
174
|
+
vts += _format_block("UNITS", c["UNITS"])
|
|
175
|
+
vts += _format_block("PATHS", c["PATHS"])
|
|
176
|
+
vts += _format_block("WAYPOINTS", c["WAYPOINTS"])
|
|
177
|
+
vts += _format_block("UNITGROUPS", c["UNITGROUPS"])
|
|
178
|
+
vts += _format_block("TimedEventGroups", "") # Aún como placeholder
|
|
179
|
+
vts += _format_block("TRIGGER_EVENTS", c["TRIGGER_EVENTS"])
|
|
180
|
+
vts += _format_block("OBJECTIVES", c["OBJECTIVES"])
|
|
181
|
+
vts += _format_block("StaticObjects", c["StaticObjects"])
|
|
182
|
+
vts += _format_block("Conditionals", self.conditionals_raw)
|
|
183
|
+
vts += _format_block("BASES", c["BASES"])
|
|
184
|
+
vts += _format_block("Briefing", c["Briefing"])
|
|
185
|
+
vts += _format_block("ResourceManifest", c["ResourceManifest"])
|
|
186
|
+
vts += "}\n"
|
|
187
|
+
|
|
188
|
+
with open(path, 'w') as f:
|
|
189
|
+
f.write(vts)
|
|
190
|
+
print(f"\n✅ Mission saved '{path}'")
|
|
191
|
+
|
|
192
|
+
def save_mission(self, base_path):
|
|
193
|
+
"""Saves the mission .vts file and associated map folder to the specified base path."""
|
|
194
|
+
mission_dir = os.path.join(base_path, self.scenario_id)
|
|
195
|
+
os.makedirs(mission_dir, exist_ok=True)
|
|
196
|
+
shutil.copytree(self.map_path, os.path.join(mission_dir, self.map_id), dirs_exist_ok=True)
|
|
197
|
+
vts_path = os.path.join(mission_dir, f"{self.scenario_id}.vts")
|
|
198
|
+
self._save_to_file(vts_path)
|
|
199
|
+
return mission_dir
|
|
200
|
+
|
|
201
|
+
|