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 ADDED
@@ -0,0 +1,5 @@
1
+ from .terrain.terrain_calculator import TerrainCalculator
2
+ from .parsers.vts_builder import Mission
3
+ from .terrain.mission_terrain_helper import MissionTerrainHelper
4
+
5
+ __version__ = "0.1.0"
@@ -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
+