├── .gitattributes ├── Maps ├── RC_Las Colinas Trailer Fix.txt └── BS_Trailer Park.txt ├── pawn.json ├── test.pwn ├── .gitignore ├── converter ├── parsers │ ├── removals.py │ ├── material.py │ ├── object.py │ └── material_text.py └── converter.py ├── mapconvert.pwn ├── README.md └── samp-map-parser.inc /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pwn linguist-language=Pawn 2 | *.inc linguist-language=Pawn 3 | -------------------------------------------------------------------------------- /Maps/RC_Las Colinas Trailer Fix.txt: -------------------------------------------------------------------------------- 1 | rmv 3298 2530.0313 -960.0391 81.2891 0.25 2 | rmv 3241 2530.0313 -960.0391 81.2891 0.25 3 | 3241 2530.03125 -960.03912 81.90910 0.00000 0.00000 92.52000 -1 -------------------------------------------------------------------------------- /pawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "TommyB123", 3 | "repo": "samp-map-parser", 4 | "entry": "test.pwn", 5 | "output": "test.amx", 6 | "dependencies": [ 7 | "openmultiplayer/omp-stdlib", 8 | "pawn-lang/samp-stdlib@open.mp", 9 | "pawn-lang/pawn-stdlib@open.mp", 10 | "samp-incognito/samp-streamer-plugin:v2.9.5", 11 | "IllidanS4/PawnPlus:v1.4.0.1", 12 | "JaTochNietDan/SA-MP-FileManager:1.5.1", 13 | "Y-Less/sscanf:v2.13.1", 14 | "oscar-broman/strlib" 15 | ], 16 | "local": true 17 | } -------------------------------------------------------------------------------- /Maps/BS_Trailer Park.txt: -------------------------------------------------------------------------------- 1 | 3172 -2509.20458984 2516.19091797 17.69790077 0.0 0.0 271.00524902 -1 2 | 3172 -2495.07275391 2514.07397461 17.27110481 0.0 0.0 180.52545166 -1 3 | 3169 -2411.14355469 2491.80883789 11.41870403 2.750 0.0 87.69995117 -1 4 | 3169 -2411.88403320 2484.30297852 11.19842339 354.74658203 0.0 268.23852539 -1 5 | 3283 -2407.26831055 2513.46069336 11.59977055 0.0 0.0 83.72998047 -1 6 | 3283 -2477.03784180 2535.82275391 17.33054733 0.0 0.0 359.99682617 -1 7 | 3169 -2460.76806641 2533.81982422 15.49490356 358.500 0.0 266.25524902 -1 -------------------------------------------------------------------------------- /test.pwn: -------------------------------------------------------------------------------- 1 | // generated by "sampctl package generate" 2 | 3 | #define MAP_PATH "./Maps/" //root directory of the git repo for test map 4 | #define MAP_FILE_EXTENSION ".txt" 5 | #include "samp-map-parser.inc" 6 | 7 | main() 8 | { 9 | // write tests for libraries here and run "sampctl package run" 10 | print("Parsing all maps"); 11 | ProcessMaps(); 12 | 13 | print("Exporting map ID 0"); 14 | ExportMap(0); 15 | 16 | print("Unloading map ID 0"); 17 | UnloadMap(0); 18 | 19 | print("Reloading all maps"); 20 | ReprocessMaps(); 21 | } 22 | 23 | public OnMapLoaded(mapid, const mapname[], List:objects) 24 | { 25 | printf("[MAP PARSER] %s (ID %i) successfully loaded. [%i objects]", mapname, mapid, list_size(objects)); 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Package only files 3 | # 4 | 5 | # Compiled Bytecode, precompiled output and assembly 6 | *.amx 7 | *.lst 8 | *.asm 9 | 10 | # Vendor directory for dependencies 11 | dependencies/ 12 | 13 | # Dependency versions lockfile 14 | pawn.lock 15 | 16 | 17 | # 18 | # Server/gamemode related files 19 | # 20 | 21 | # compiled settings file 22 | # keep `samp.json` file on version control 23 | # but make sure the `rcon_password` field is set externally 24 | # you can use the environment variable `SAMP_RCON_PASSWORD` to do this. 25 | server.cfg 26 | 27 | # Plugins directory 28 | plugins/ 29 | 30 | # binaries 31 | *.exe 32 | *.dll 33 | *.so 34 | announce 35 | samp03svr 36 | samp-npc 37 | 38 | # logs 39 | logs/ 40 | server_log.txt 41 | crashinfo.txt 42 | 43 | # Ban list 44 | samp.ban 45 | 46 | # 47 | # Common files 48 | # 49 | 50 | *.sublime-workspace -------------------------------------------------------------------------------- /converter/parsers/removals.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class SampBuildingRemoval: 5 | remove_modelid: int = 0 6 | remove_x: float = 0.0 7 | remove_y: float = 0.0 8 | remove_z: float = 0.0 9 | remove_radius: float = 0.0 10 | 11 | def parse_building_removal_line(line: str) -> SampBuildingRemoval: 12 | """Extract and return building removal attributes from a 'RemoveBuildingForPlayer' line.""" 13 | try: 14 | line = line.strip().replace('RemoveBuildingForPlayer(playerid,', '').replace(');', '') 15 | params = [param.strip() for param in line.split(',')] 16 | 17 | return SampBuildingRemoval( 18 | remove_modelid=int(params[0]), 19 | remove_x=float(params[1]), 20 | remove_y=float(params[2]), 21 | remove_z=float(params[3]), 22 | remove_radius=float(params[4]) 23 | ) 24 | except (IndexError, ValueError) as e: 25 | print(f"Error processing line: {line}\n{e}") 26 | return None 27 | -------------------------------------------------------------------------------- /converter/parsers/material.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import re 3 | 4 | @dataclass 5 | class SampObjectMaterial: 6 | material_index: int = 0 7 | material_model: int = 0 8 | material_txd: str = "" 9 | material_texture: str = "" 10 | material_color: str = "" 11 | 12 | def parse_material_line(line: str) -> SampObjectMaterial: 13 | """Extract and return material attributes from a 'SetDynamicObjectMaterial' line.""" 14 | try: 15 | # Remove the 'SetDynamicObjectMaterial(' part and trailing ')' 16 | line = re.sub(r'^.*SetDynamicObjectMaterial\(([^,]+),', '', line) 17 | line = re.sub(r'\);\s*(\w+)?', '', line) 18 | params = line.split(', ') 19 | 20 | material = SampObjectMaterial( 21 | material_index=int(params[0]), 22 | material_model=int(params[1]), 23 | material_txd=params[2].replace('"', ''), 24 | material_texture=params[3].replace('"', ''), 25 | material_color=params[4] 26 | ) 27 | 28 | # sanitize some of the received map data so we don't have to do it when outputting compatible map code 29 | if (material.material_model == 16644 30 | and material.material_txd == 'a51_detailstuff' 31 | and material.material_texture == 'roucghstonebrtb' 32 | ): 33 | material.material_model = 18888 34 | material.material_txd = "forcefields" 35 | material.material_texture = "white" 36 | 37 | if ' ' in material.material_texture: 38 | # material has a space in it, replace it with an amazing key word for the map script to identify 39 | material.material_texture = material.material_texture.replace(' ', 'putafuckingspacehere') 40 | 41 | return material 42 | except (IndexError, ValueError) as e: 43 | print(f"Error processing line: {line}\n{e}") 44 | return None 45 | -------------------------------------------------------------------------------- /converter/parsers/object.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass, field 3 | from typing import List 4 | 5 | @dataclass 6 | class SampObject: 7 | modelid: int = 0 8 | x: float = 0.0 9 | y: float = 0.0 10 | z: float = 0.0 11 | rx: float = 0.0 12 | ry: float = 0.0 13 | rz: float = 0.0 14 | worldid: int = -1 15 | interior: int = -1 16 | player: int = -1 17 | streamdist: float = 300.0 18 | drawdist: float = 300.0 19 | areaid: int = -1 20 | priority: int = 0 21 | materials: List = field(default_factory=list) 22 | materialtext: List = field(default_factory=list) 23 | 24 | def parse_dynamic_object_line(line: str, **defaults) -> SampObject: 25 | """Extract and return attributes from a 'CreateDynamicObject' line.""" 26 | line = re.sub(r'^.*CreateDynamicObject\(|\);\s*(\w+)?', '', line) 27 | params = [param.strip() for param in line.split(',')] 28 | 29 | try: 30 | return SampObject( 31 | modelid=int(params[0]), 32 | x=float(params[1]), 33 | y=float(params[2]), 34 | z=float(params[3]), 35 | rx=float(params[4]), 36 | ry=float(params[5]), 37 | rz=float(params[6]), 38 | worldid=int(params[7]) if len(params) > 7 else defaults.get('default_worldid', -1), 39 | interior=int(params[8]) if len(params) > 8 else defaults.get('default_interior', -1), 40 | player=int(params[9]) if len(params) > 9 else defaults.get('default_player', -1), 41 | streamdist=float(params[10]) if len(params) > 10 else defaults.get('default_streamdist', 300.0), 42 | drawdist=float(params[11]) if len(params) > 11 else defaults.get('default_drawdist', 300.0), 43 | areaid=int(params[12]) if len(params) > 12 else defaults.get('default_areaid', -1), 44 | priority=int(params[13]) if len(params) > 13 else defaults.get('default_priority', 0) 45 | ) 46 | except (IndexError, ValueError) as e: 47 | print(f"Error processing line: {line}\n{e}") 48 | return None 49 | -------------------------------------------------------------------------------- /converter/parsers/material_text.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import re 3 | 4 | @dataclass 5 | class SampObjectMaterialText: 6 | text_index: int = 0 7 | text_contents: str = "" 8 | text_materialsize: int = 256 9 | text_font: str = "Arial" 10 | text_fontsize: int = 24 11 | text_bold: int = 1 12 | text_fontcolor: int = 0xFFFFFFFF 13 | text_backgroundcolor: int = 0 14 | text_alignment: int = 0 15 | 16 | def parse_material_text_line(line: str) -> SampObjectMaterialText: 17 | """Extract and return material text attributes from a 'SetDynamicObjectMaterialText' line.""" 18 | try: 19 | # Remove the 'SetDynamicObjectMaterialText(' part and trailing ')' 20 | line = re.sub(r'^.*SetDynamicObjectMaterialText\(([^,]+),', '', line) 21 | line = re.sub(r'\);\s*(\w+)?', '', line) 22 | params = line.split(', ') 23 | 24 | # Ensure we have at least the required parameters 25 | if len(params) < 3: 26 | print(f'Expected at least 3 params when parsing SetDynamicObjectMaterialText but found {len(params)}. Ending conversion.') 27 | return None 28 | 29 | # Fill in optional parameters with defaults if not provided 30 | while len(params) < 9: 31 | params.append('') 32 | 33 | return SampObjectMaterialText( 34 | text_index=int(params[0]), 35 | text_contents=params[1].replace('"', '').replace('\n', '~N~'), 36 | text_materialsize=int(params[2]) if params[2] else 256, 37 | text_font=params[3].replace(' ', '_').replace('"', '') if params[3] else 'Arial', 38 | text_fontsize=int(params[4]) if params[4] else 24, 39 | text_bold=int(params[5]) if params[5] else 1, 40 | text_fontcolor=int(params[6], 16) if params[6] else 0xFFFFFFFF, 41 | text_backgroundcolor=int(params[7], 16) if params[7] else 0, 42 | text_alignment=int(params[8]) if params[8] else 0 43 | ) 44 | except (IndexError, ValueError) as e: 45 | print(f"Error processing line: {line}\n{e}") 46 | return None 47 | -------------------------------------------------------------------------------- /mapconvert.pwn: -------------------------------------------------------------------------------- 1 | #define FILTERSCRIPT 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | CreateMap() 9 | { 10 | //put your map code here, compile the script and then launch the SA-MP server with this filterscript loaded. 11 | //the map you input will be written to convertedmap.txt in a format compatible with the parser. 12 | } 13 | 14 | public OnFilterScriptInit() 15 | { 16 | new str[2056]; 17 | 18 | //create the map objects. 19 | CreateMap(); 20 | 21 | //if a converted map file already exists, delete it. created another no matter what 22 | if(file_exists("convertedmap.txt")) file_delete("convertedmap.txt"); 23 | file_create("convertedmap.txt"); 24 | 25 | //open the file 26 | new File:handle = f_open("convertedmap.txt", "w"); 27 | 28 | new model, Float:x, Float:y, Float:z, Float:rx, Float:ry, Float:rz, world, InitialTicks = GetTickCount(); 29 | for(new i = 1; i <= Streamer_GetUpperBound(STREAMER_TYPE_OBJECT); i++) 30 | { 31 | if(!IsValidDynamicObject(i)) continue; 32 | 33 | //grab the object model, world and position 34 | model = Streamer_GetIntData(STREAMER_TYPE_OBJECT, i, E_STREAMER_MODEL_ID); 35 | world = Streamer_GetIntData(STREAMER_TYPE_OBJECT, i, E_STREAMER_WORLD_ID); 36 | GetDynamicObjectPos(i, x, y, z); 37 | GetDynamicObjectRot(i, rx, ry, rz); 38 | 39 | //print the object data 40 | format(str, sizeof(str), "%i %f %f %f %f %f %f %i\n", model, x, y, z, rx, ry, rz, world); 41 | f_write(handle, str); 42 | 43 | //check object for any materials, print them if they exist 44 | for(new m = 0; m < 15; m++) 45 | { 46 | if(IsDynamicObjectMaterialUsed(i, m)) 47 | { 48 | new tmodel, txdname[64], texturename[64], materialcolor; 49 | GetDynamicObjectMaterial(i, m, tmodel, txdname, texturename, materialcolor); 50 | 51 | //always replace this texture because it's shit and doesn't work in interiors 52 | if(tmodel == 16644 && !strcmp(txdname, "a51_detailstuff") && !strcmp(texturename, "roucghstonebrtb")) 53 | { 54 | tmodel = 18888; 55 | txdname = "forcefields"; 56 | texturename = "white"; 57 | } 58 | if(strfind(texturename, " ") != -1) strreplace(texturename, " ", "putafuckingspacehere"); 59 | 60 | format(str, sizeof(str), "mat %i %i %s %s 0x%08x\n", m, tmodel, txdname, texturename, materialcolor); 61 | f_write(handle, str); 62 | } 63 | } 64 | 65 | //check for any material texts, print them if they exist 66 | for(new m = 0; m < 15; m++) 67 | { 68 | if(IsDynamicObjectMaterialTextUsed(i, m)) 69 | { 70 | new text[128], materialsize, fontface[32], fontsize, bold, fontcolor, backcolor, alignment; 71 | GetDynamicObjectMaterialText(i, m, text, materialsize, fontface, fontsize, bold, fontcolor, backcolor, alignment); 72 | 73 | strreplace(fontface, " ", "_"); 74 | strreplace(text, "\n", "~N~"); 75 | format(str, sizeof(str), "txt %i %i %s %i %i 0x%x 0x%08x %i %s\n", m, materialsize, fontface, fontsize, bold, fontcolor, backcolor, alignment, text); 76 | f_write(handle, str); 77 | } 78 | } 79 | } 80 | if(Streamer_GetUpperBound(STREAMER_TYPE_OBJECT) != 1) printf("Map exported to convertedmap.txt in %i milliseconds.", GetTickCount() - InitialTicks); 81 | f_close(handle); 82 | } -------------------------------------------------------------------------------- /converter/converter.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | import unicodedata 4 | from typing import Tuple, List 5 | from pathlib import Path 6 | 7 | from parsers.object import parse_dynamic_object_line, SampObject 8 | from parsers.material import parse_material_line 9 | from parsers.material_text import parse_material_text_line 10 | from parsers.removals import parse_building_removal_line, SampBuildingRemoval 11 | 12 | path = Path(__file__).parent.parent 13 | 14 | parser = argparse.ArgumentParser(description="Convert streamer map files to samp-mapparser format.") 15 | parser.add_argument( 16 | "-i", "--input", 17 | help="Input directory containing map files", 18 | type=str, 19 | default=str(path / 'map_sources') 20 | ) 21 | parser.add_argument( 22 | "-o", "--output", 23 | help="Output directory for converted maps", 24 | type=str, 25 | default=str(path / 'scriptfiles' / 'maps') 26 | ) 27 | parser.add_argument( 28 | "--input-ext", 29 | help="Input file extension", 30 | type=str, 31 | default=".txt" 32 | ) 33 | parser.add_argument( 34 | "--output-ext", 35 | help="Output file extension", 36 | type=str, 37 | default=".txt" 38 | ) 39 | parser.add_argument( 40 | "--file", 41 | help="Process only a single file instead of directory", 42 | type=str 43 | ) 44 | parser.add_argument( 45 | "--interior", 46 | help="Override interior ID for all objects", 47 | type=int, 48 | default=None 49 | ) 50 | parser.add_argument( 51 | "--world", 52 | help="Override world ID for all objects", 53 | type=int, 54 | default=None 55 | ) 56 | parser.add_argument( 57 | "--priority", 58 | help="Override priority for all objects", 59 | type=int, 60 | default=None 61 | ) 62 | args = parser.parse_args() 63 | 64 | input_directory = Path(args.input) 65 | output_directory = Path(args.output) 66 | map_input_file_extension = args.input_ext 67 | map_output_file_extension = args.output_ext 68 | 69 | 70 | def parse_map(map_input_file: Path) -> Tuple[List[SampBuildingRemoval], List[SampObject]]: 71 | """Parse a map file and return buildings and objects.""" 72 | try: 73 | with open(map_input_file, 'r', encoding='utf-8') as file: 74 | buildings = [] 75 | objects = [] 76 | 77 | for line_number, line in enumerate(file, 1): 78 | try: 79 | line = line.strip() 80 | if not line: 81 | continue 82 | 83 | if 'CreateDynamicObject(' in line: 84 | obj = parse_dynamic_object_line(line) 85 | if obj: 86 | objects.append(obj) 87 | elif 'SetDynamicObjectMaterial(' in line: 88 | material = parse_material_line(line) 89 | if material: 90 | objects[-1].materials.append(material) 91 | elif 'SetDynamicObjectMaterialText(' in line: 92 | text = parse_material_text_line(line) 93 | if text: 94 | objects[-1].materialtext.append(text) 95 | elif 'RemoveBuildingForPlayer(' in line: 96 | building = parse_building_removal_line(line) 97 | buildings.append(building) 98 | else: 99 | # Ignore lines that don't need to be parsed 100 | continue 101 | except Exception as e: 102 | print(f"Error processing line {line_number}: {e}") 103 | 104 | return buildings, objects 105 | except Exception as e: 106 | print(f"Failed to parse map file {map_input_file}: {e}") 107 | raise 108 | 109 | def write_converted_map(map_output_file: Path, buildings, objects): 110 | try: 111 | with open(map_output_file, 'a+') as file_output_handler: 112 | # Write building removal data 113 | for building in buildings: 114 | file_output_handler.write( 115 | f'rmv {building.remove_modelid} {building.remove_x:.6f} {building.remove_y:.6f} {building.remove_z:.6f} {building.remove_radius:.2f}\n' 116 | ) 117 | 118 | # Write mapped objects 119 | for obj in objects: 120 | file_output_handler.write( 121 | f'{obj.modelid} {obj.x:.6f} {obj.y:.6f} {obj.z:.6f} {obj.rx:.6f} {obj.ry:.6f} {obj.rz:.6f} {obj.worldid} {obj.interior} {obj.player} {obj.streamdist:.6f} {obj.drawdist:.6f} {obj.areaid} {obj.priority}\n' 122 | ) 123 | 124 | if len(obj.materials) > 0: 125 | for material in obj.materials: 126 | file_output_handler.write( 127 | f'mat {material.material_index} {material.material_model} {material.material_txd} {material.material_texture} {material.material_color}\n' 128 | ) 129 | 130 | if len(obj.materialtext) > 0: 131 | for text in obj.materialtext: 132 | file_output_handler.write( 133 | f'txt {text.text_index} {text.text_materialsize} {text.text_font} {text.text_fontsize} {text.text_bold} {text.text_fontcolor} {text.text_backgroundcolor} {text.text_alignment} {text.text_contents}\n' 134 | ) 135 | except Exception as e: 136 | print(f"Failed to write converted map to {map_output_file}: {e}") 137 | raise 138 | 139 | 140 | def convert_map(map_name: str): 141 | # Convert map_name to ANSI-compatible format as samp may struggle 142 | map_name_normalized = unicodedata.normalize('NFKD', map_name).encode('ASCII', 'ignore').decode('ASCII') 143 | map_name_ansi = re.sub(r'[^\w.-]', '', map_name_normalized.replace(' ', '_')) 144 | 145 | map_input_file = Path(input_directory) / (map_name + map_input_file_extension) 146 | map_output_file = output_directory / (map_name_ansi + map_output_file_extension) 147 | 148 | # delete output file if it already exists so we don't overwrite or append to converted map data 149 | if map_output_file.is_file(): 150 | map_output_file.unlink() 151 | 152 | buildings, objects = parse_map(map_input_file) 153 | 154 | # Custom parameters if specified 155 | if args.interior is not None: 156 | for obj in objects: 157 | obj.interior = args.interior 158 | 159 | if args.world is not None: 160 | for obj in objects: 161 | obj.worldid = args.world 162 | 163 | if args.priority is not None: 164 | for obj in objects: 165 | obj.priority = args.priority 166 | 167 | output_directory.mkdir(parents=True, exist_ok=True) 168 | write_converted_map(map_output_file, buildings, objects) 169 | 170 | print(f'The converted map has been written to: {map_output_file}') 171 | 172 | 173 | def convert_dir(directory: Path) -> None: 174 | """Convert all map files in directory.""" 175 | converted_count = 0 176 | error_count = 0 177 | try: 178 | for file_path in directory.glob(f"*{map_input_file_extension}"): 179 | print(f'Converting: {file_path.name}') 180 | try: 181 | convert_map(file_path.stem) 182 | converted_count += 1 183 | except Exception as e: 184 | print(f"Error converting {file_path.name}: {e}") 185 | error_count += 1 186 | except Exception as e: 187 | print(f"Failed to convert directory {directory}: {e}") 188 | raise 189 | finally: 190 | print(f"Conversion completed. Total maps converted: {converted_count}") 191 | if error_count > 0: 192 | print(f"Total errors encountered: {error_count}") 193 | 194 | if args.file: 195 | file_path = Path(args.file) 196 | if not file_path.is_file(): 197 | print(f"Error: File not found - {file_path}") 198 | else: 199 | print(f'Converting single file: {file_path.name}') 200 | convert_map(file_path.stem) 201 | else: 202 | convert_dir(input_directory) 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## As of version 1.2.0, this library ONLY supports the open.mp libraries. For SA-MP support, refer to older versions. 2 | 3 | # samp-map-parser 4 | 5 | [![sampctl](https://img.shields.io/badge/sampctl-samp--map--parser-2f2f2f.svg?style=for-the-badge)](https://github.com/TommyB123/samp-map-parser) 6 | 7 | This is a library I wrote for Red County Roleplay to quickly parse individual SA-MP maps on server boot. An easy-to-use API is also provided to manipulate maps (load, unload, reload, etc) during server runtime. 8 | 9 | **This library is not very beginner friendly as a result of maps being stored in a non-standard, CSV-like format. It is also rather dependency heavy, so be wary if that's something that bothers you.** 10 | 11 | ## Dependencies 12 | * [Streamer Plugin](https://github.com/samp-incognito/samp-streamer-plugin) 13 | * [PawnPlus](https://github.com/IllidanS4/PawnPlus) 14 | * [FileManager](https://github.com/JaTochNietDan/SA-MP-FileManager) 15 | * [sscanf2](https://github.com/Y-Less/sscanf) 16 | * [sstrlib](https://github.com/oscar-broman/strlib) 17 | 18 | ## Installation 19 | 20 | Simply install to your project: 21 | 22 | ```bash 23 | sampctl install TommyB123/samp-map-parser 24 | ``` 25 | 26 | Include in your code and begin using the library: 27 | 28 | ```pawn 29 | #include 30 | ``` 31 | 32 | ## Converting a map 33 | Before you even begin, you'll need a compatible map file. All map data is stored in a CSV-like format, so converting code from the usual SA-MP functions (`CreateObject`, `CreateDynamicObject`) is required in order to load maps with this library. Two conversion scripts are provided as-is, one being a CLI script written in Python, and the other is a SA-MP filterscript. Both of these scripts will quickly convert standard SA-MP map code to parser-compatible code. (I personally recommend using the Python script for converting multiple maps) 34 | 35 | **Standard SA-MP object functions are not supported by the conversion scripts. Only streamer plugin functions are.** 36 | 37 | The parser (obviously) supports objects, as well as materials, material text and removed buildings. Every argument in `CreateDynamicObject` is supported (except linking to streamer areas), so arbitrary virtual worlds and stream/draw distance can be carried over easily. 38 | 39 | Here's an example of what a compatible map file looks like. Two small example files are also present in the `Maps` folder of the repository. 40 | 41 | ``` 42 | rmv 1440 1252.375 247.6125 19.046 7.8313135570295485 43 | rmv 13010 1258.25 245.5156 27.5625 0.25 44 | 11504 1252.721191 242.208541 18.554687 0.000000 0.000000 -114.700004 -1 45 | mat 0 1736 cj_ammo CJ_SLATEDWOOD2 0xFFFFFFFF 46 | mat 1 10101 2notherbuildsfe Bow_Abpave_Gen 0x00000000 47 | mat 2 1736 cj_ammo CJ_CANVAS2 0x00000000 48 | 19447 1251.597656 249.466583 20.204685 0.000000 0.000000 -24.700004 -1 49 | mat 0 12946 ce_bankalley1 sw_brick03 0x00000000 50 | 19447 1257.887329 251.823577 20.204685 0.000000 0.000000 65.299995 -1 51 | mat 0 12946 ce_bankalley1 sw_brick03 0x00000000 52 | 19447 1247.573608 240.717590 20.204685 0.000000 0.000000 -24.700004 -1 53 | mat 0 12946 ce_bankalley1 sw_brick03 0x00000000 54 | 1569 1248.763671 243.503890 18.554687 0.000000 0.000000 64.899993 -1 55 | mat 0 16093 a51_ext des_backdoor1 0xFFFFFFFF 56 | 19863 1252.053100 250.648269 21.036880 0.000000 0.000000 65.299995 -1 57 | mat 0 -1 none none 0xFFFFFFFF 58 | 19368 1250.020385 246.661697 16.814670 0.000000 0.000000 -24.999998 -1 59 | mat 0 19517 noncolored gen_white 0x00000000 60 | 19368 1249.118164 248.748794 16.814653 180.000000 180.000000 65.000000 -1 61 | mat 0 19517 noncolored gen_white 0x00000000 62 | 19368 1247.837158 246.002655 16.814653 180.000000 180.000000 65.000000 -1 63 | mat 0 19517 noncolored gen_white 0x00000000 64 | ``` 65 | 66 | ### Using the Python conversion script 67 | 68 | #### Prerequisites 69 | * Ensure Python is available on your system. Any modern version (>3.5) should work. The script has been tested with Python 3.8. 70 | 71 | #### Steps 72 | 1. **Prepare Input File(s):** 73 | * Place your desired map code into a text file with the `.txt` extension. 74 | * You can use maps created in [Texture Studio](https://github.com/Pottus/Texture-Studio) 75 | * By default, the script looks for files in the `map_sources` directory. 76 | 77 | 2. **Run the Script:** 78 | * Open a terminal/command line window in the appropriate directory. 79 | * To convert all files in the `map_sources` directory: 80 | ```sh 81 | python converter/converter.py 82 | ``` 83 | * To convert a single file: 84 | ```sh 85 | python converter/converter.py --file path/to/your/input.txt 86 | ``` 87 | 88 | #### Script Arguments 89 | * `-i`, `--input`: Input directory containing map files (default: `map_sources`) 90 | * `-o`, `--output`: Output directory for converted maps (default: `scriptfiles/maps`) 91 | * `--input-ext`: Input file extension (default: `.txt`) 92 | * `--output-ext`: Output file extension (default: `.txt`) 93 | * `--file`: Process only a single file instead of a directory 94 | * `--interior`: Override interior ID for all objects 95 | * `--world`: Override world ID for all objects 96 | * `--priority`: Override priority value for all objects 97 | 98 | #### Notes 99 | * If the `--file` argument is used, the script will process only the specified file. 100 | * If the `--interior`, `--world`, or `--priority` arguments are provided, they will override the respective attributes for all objects in the map. 101 | * The script will output the converted map files to the specified output directory with the `.txt` extension by default. 102 | 103 | ### Using the PAWN Filterscript 104 | * Open `mapconvert.pwn` in your editor of choice. 105 | * Paste your map's code into the `CreateMap` function at the top of the script. 106 | * Compile the script and then run it as a SA-MP filterscript. 107 | * If the filterscript ran successfully, your map will be available in the SA-MP server's root directory as `convertedmap.txt`. 108 | 109 | ## Loading the maps in-game 110 | 111 | By default, the script will attempt to parse any `.txt` file stored inside of a `Maps` folder in your server's root directory. These can both be redefined with constants provided below. 112 | 113 | To load each file inside of the `Maps` directory, you simply need to run the `ProcessMaps` function. 114 | 115 | For example: 116 | 117 | ```pawn 118 | public OnGameModeInit() 119 | { 120 | // load all maps on server startup 121 | ProcessMaps(); 122 | } 123 | ``` 124 | 125 | If you just want to cold load maps on server startup without having to worry about recompiling your source code, you don't need to do anything else. However, as alluded to above, there's a handful of extra functions to allow for further control over your server's maps. 126 | 127 | For example, you can quickly write up commands for high-level staff to load maps on the fly. 128 | 129 | ```pawn 130 | CMD:loadmap(playerid, params[]) 131 | { 132 | if(!IsPlayerAdmin(playerid)) return SysMsg(playerid, "Unauthorized."); 133 | if(isnull(params)) return SysMsg(playerid, "Usage: /loadmap [map name]"); 134 | if(!IsValidMapFile(params)) return SysMsg(playerid, "Invalid map name."); 135 | if(GetMapID(params) != INVALID_MAP_ID) return SysMsg(playerid, "The map '%s' is already loaded.", params); 136 | 137 | LoadMap(params); 138 | SysMsg(playerid, "You have loaded the map '%s' onto the server.", params); 139 | return true; 140 | } 141 | ``` 142 | 143 | ## Functions 144 | 145 | | Function | Description | 146 | | - | - | 147 | | `LoadMap(mapname[])` | Attempts to load a file in the map directory using the specified file name. | 148 | | `UnloadMap(mapid)` | Unload a map that is present on the server. (Requires fetching a map's current ID beforehand). All objects associated with the map file are deleted, and if any `RemoveBuildingForPlayer` instructions are contained, they will no longer be applied when new players connect. 149 | | `SkipCurrentlyLoadingMap()` | Skips any map that is currently loading and completely unloads it. Designed to be used with the `OnMapBeginLoading` callback for more control over what maps are fully loaded at server startup. 150 | | `ReprocessMaps()` | Unloads every single map present on the server then parses every available map file again. | 151 | | `GetMapNameFromID(mapid)` | Resolves a map name from an ID. Returns the map's name as a string. | 152 | | `GetMapIDFromName(const name[])` | Resolves a map's ID from a string. Returns the map ID or `INVALID_MAP_ID` if no map is found. | 153 | | `GetMapIDFromObject(STREAMER_TAG_OBJECT:objectid)` | Resolves a map's ID from a dynamic object ID. Returns the map ID or `INVALID_MAP_ID` if no map is linked to the provided object ID. | 154 | | `GetMapCount()` | Returns the amount of maps currently loaded through the parser script. | 155 | | `GetMapList(String:string, bool:order = false)` | Formats a PawnPlus string with a list of currently loaded maps and how many objects each of them has. `order` dictates whether the list will be sorted by map object count (descending). | 156 | | `IsValidMapFile(const mapname[])` | Checks the map folder for a file with the provided name. Returns `true` if found, `false` if not. 157 | | `ExportMap(mapid)` | Exports a currently-loaded map back to standard SA-MP object code and writes it to a file in the server's root directory. | 158 | | `MoveMapObjects(mapid, Float:xoffset, Float:yoffset, Float:zoffset)` | Temporarily moves the objects belonging to a specific map on the X, Y or Z axis. Not particularly useful, but I needed it for an edge case once or twice. | 159 | | `public OnMapLoaded(mapid, const mapname[], List:objects)` | This callback is triggered whenever a map is loaded.
`mapid` is the temporary ID of the map.
`mapname` is the name of the map.
`List:objects` is a PawnPlus list containing references to every object ID belonging to the map. | 160 | | `public OnMapBeginLoading(mapid, const mapname[], firstobjectworldid)` | This callback is triggered before the first object in a map has been created.
`mapid` is the temporary ID of the map.
`mapname` is the name of the map.
`firstobjectworldid` is the virtual world of the first object that is about to be created. 161 | 162 | ## Relevant constants 163 | 164 | | Constant | Default Value | Description | 165 | | ------------- | ------------- | ------------- | 166 | | `MAP_FILE_DIRECTORY` | `./Maps/` | The directory the library will search for map files in. | 167 | | `MAP_FILE_EXTENSION` | `txt` | The file extension that the library will open when parsing map files. | 168 | | `MAX_MAP_NAME` | `64` | The maximum amount of characters the name of a map can contain. | 169 | | `INVALID_MAP_ID` | `-1` | Used to refer to an invalid map ID. | 170 | 171 | ## Questions I'll probably get 172 | **Q:** Why does this library use a custom format that requires conversion rather than simply parsing object functions from text files? 173 | 174 | **A:** I stole this method from a friend's server, and by the time I realized it was kind of silly, it was too late to go back and I didn't care to rewrite the parsing code. There's a slight speed benefit to this method since there's less repetitive text to parse, which is handy when you're dealing with hundreds of thousands of objects and huge maps with thousands of lines in each file. 175 | 176 | 177 | **Q:** Why doesn't something work in a way I think would be better? 178 | 179 | **A:** I wrote this library to suit my needs and development practices for a server I ran for 8 years. If you have an idea that would improve the library or the conversion scripts, you're more than welcome to toss up a pull request. 180 | 181 | ## Credits 182 | me 183 | -------------------------------------------------------------------------------- /samp-map-parser.inc: -------------------------------------------------------------------------------- 1 | #if defined __samp_map_parser_included 2 | #endinput 3 | #endif 4 | #define __samp_map_parser_included 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if !defined MAX_MAP_NAME 14 | #define MAX_MAP_NAME (64) 15 | #endif 16 | 17 | #define INVALID_MAP_ID -1 18 | 19 | #define MAP_OBJ_EXTRA_OFFSET (0) 20 | #define MAP_OBJ_EXTRA_MAPID (1) 21 | 22 | #if !defined MAP_FILE_DIRECTORY 23 | #define MAP_FILE_DIRECTORY "./Maps/" 24 | #endif 25 | 26 | #if !defined MAP_FILE_EXTENSION 27 | #define MAP_FILE_EXTENSION ".txt" 28 | #endif 29 | 30 | static enum _:REMOVE_DATA 31 | { 32 | removemodel, 33 | Float:removex, 34 | Float:removey, 35 | Float:removez, 36 | Float:removeradius 37 | }; 38 | 39 | //constants for map data 40 | #define MAP_KEY_NAME "MAP_KEY_NAME" 41 | #define MAP_KEY_OBJECT_LIST "MAP_KEY_OBJECT_LIST" 42 | #define MAP_KEY_REMOVED_BUILDING_LIST "MAP_KEY_REMOVED_BUILDING_LIST" 43 | 44 | static Pool:Maps; 45 | static bool:ProcessingMaps; 46 | static bool:SkipMap; 47 | 48 | forward OnMapLoaded(mapid, const mapname[], List:objects); 49 | forward OnMapBeginLoading(mapid, const mapname[], firstobjectworldid); 50 | 51 | public OnPlayerConnect(playerid) 52 | { 53 | if(!IsPlayerNPC(playerid)) ProcessRemovedBuildings(playerid); 54 | 55 | #if defined MAP_OnPlayerConnect 56 | return MAP_OnPlayerConnect(playerid); 57 | #else 58 | return 1; 59 | #endif 60 | } 61 | 62 | stock ProcessMaps() 63 | { 64 | if(!pool_valid(Maps)) 65 | { 66 | Maps = pool_new(.ordered = true); 67 | } 68 | 69 | if(!dir_exists(MAP_FILE_DIRECTORY)) 70 | { 71 | printf("[MAP PARSER] Map directory (%s) was not found. Creating directory and cancelling loading.", MAP_FILE_DIRECTORY); 72 | ProcessingMaps = false; 73 | dir_create(MAP_FILE_DIRECTORY); 74 | return true; 75 | } 76 | 77 | ProcessingMaps = true; 78 | new InitialTicks = GetTickCount(); 79 | 80 | //read through all map files in the directory and load them 81 | new dir:MapsDir = dir_open(MAP_FILE_DIRECTORY), fname[MAX_MAP_NAME], ftype; 82 | while(dir_list(MapsDir, fname, ftype)) 83 | { 84 | if(ftype == FM_DIR) continue; 85 | if(strfind(fname, MAP_FILE_EXTENSION) == -1) continue; 86 | 87 | LoadMap(fname); 88 | } 89 | dir_close(MapsDir); 90 | 91 | new mapcount = pool_size(Maps); 92 | printf("[MAP PARSER] Successfully loaded %i %s. [%ims, %i objects, %i removed buildings]\n", mapcount, mapcount == 1 ? "map" : "maps", GetTickCount() - InitialTicks, CountMapObjects(), CountRemovedBuildings()); 93 | ProcessingMaps = false; 94 | return true; 95 | } 96 | 97 | stock LoadMap(mapname[]) 98 | { 99 | if(strlen(mapname) > MAX_MAP_NAME) 100 | { 101 | printf("[MAP PARSER] Map name '%s' is too long.", mapname); 102 | return INVALID_MAP_ID; 103 | } 104 | 105 | new Map:data = map_new(), name[MAX_MAP_NAME]; 106 | format(name, MAX_MAP_NAME, mapname); 107 | new index = strfind(mapname, MAP_FILE_EXTENSION); 108 | if(index != -1) 109 | { 110 | name[index] = EOS; //trimn the file extension from the map name 111 | } 112 | map_str_add_str(data, MAP_KEY_NAME, name); 113 | 114 | new mapid = GetMapIDFromName(name); 115 | if(mapid != INVALID_MAP_ID) 116 | { 117 | map_delete(data); 118 | printf("[MAP PARSER] Attempted to load map '%s' while it was already loaded.", mapname); 119 | return mapid; 120 | } 121 | 122 | if(!pool_valid(Maps)) 123 | { 124 | Maps = pool_new(); 125 | pool_set_ordered(Maps, true); 126 | } 127 | 128 | new List:objects = list_new(); 129 | map_str_add(data, MAP_KEY_OBJECT_LIST, objects); 130 | 131 | new List:buildings = list_new(); 132 | map_str_add(data, MAP_KEY_REMOVED_BUILDING_LIST, buildings); 133 | 134 | new mapindex = pool_add(Maps, data); 135 | 136 | //string for formatting 137 | new templine[256]; 138 | 139 | //file variables 140 | format(templine, sizeof(templine), "%s%s%s", MAP_FILE_DIRECTORY, name, MAP_FILE_EXTENSION); 141 | new File:f = f_open(templine, "r"); 142 | 143 | //object variables 144 | new STREAMER_TAG_OBJECT:tempobj, model, Float:x, Float:y, Float:z, Float:rx, Float:ry, Float:rz, world, interior, playerid, Float:streamdist, Float:drawdist, areaid, priority; 145 | 146 | //object material variables 147 | new matindex, matmodel, mattxd[64], mattexture[64], matcolor; 148 | 149 | //object material text variables 150 | new mattext[256], matsize, matfont[32], matfontsize, matbold, matbgcolor, matalign; 151 | 152 | //streamer extra ID array 153 | new mapobject[2]; 154 | 155 | //building removal data 156 | new remove[REMOVE_DATA]; 157 | 158 | while(f_read(f, templine)) 159 | { 160 | if(!templine[0]) continue; 161 | 162 | if(SkipMap) 163 | { 164 | SkipMap = false; 165 | UnloadMap(mapindex); 166 | f_close(f); 167 | return INVALID_MAP_ID; 168 | } 169 | 170 | if(templine[0] == 'r') 171 | { 172 | sscanf(templine, "{s[5]}iffff", remove[removemodel], remove[removex], remove[removey], remove[removez], remove[removeradius]); 173 | #if defined CA_RemoveBuilding 174 | CA_RemoveBuilding(remove[removemodel], remove[removex], remove[removey], remove[removez], remove[removeradius]); 175 | #endif 176 | list_add_arr(buildings, remove); 177 | continue; 178 | } 179 | else if(templine[0] == 'm') 180 | { 181 | sscanf(templine, "{s[5]}iis[64]s[64]N(0)", matindex, matmodel, mattxd, mattexture, matcolor); 182 | strreplace(mattexture, "putafuckingspacehere", " ", true); 183 | SetDynamicObjectMaterial(tempobj, matindex, matmodel, mattxd, mattexture, matcolor); 184 | continue; 185 | } 186 | else if(templine[0] == 't') 187 | { 188 | sscanf(templine, "{s[5]}iis[32]iinnis[256]", matindex, matsize, matfont, matfontsize, matbold, matcolor, matbgcolor, matalign, mattext); 189 | strreplace(matfont, "_", " "); 190 | strreplace(mattext, "~N~", "\n", true); 191 | strreplace(mattext, "putafuckingspacehere", " ", true); 192 | SetDynamicObjectMaterialText(tempobj, matindex, mattext, matsize, matfont, matfontsize, matbold, matcolor, matbgcolor, matalign); 193 | continue; 194 | } 195 | 196 | //parse an object 197 | sscanf(templine, "iffffffiI(-1)I(-1)F(300.0)F(300.0)I(-1)I(0)", model, x, y, z, rx, ry, rz, world, interior, playerid, streamdist, drawdist, areaid, priority); 198 | if(!list_size(objects)) 199 | { 200 | //call OnMapBeginLoading before the first object is created 201 | pawn_call_public("OnMapBeginLoading", "isi", mapid, mapname, world); 202 | } 203 | tempobj = CreateDynamicObject(model, x, y, z, rx, ry, rz, world, interior, playerid, streamdist, drawdist, STREAMER_TAG_AREA:areaid, priority); 204 | 205 | mapobject[MAP_OBJ_EXTRA_OFFSET] = -5; 206 | mapobject[MAP_OBJ_EXTRA_MAPID] = mapindex; 207 | Streamer_SetArrayData(STREAMER_TYPE_OBJECT, tempobj, E_STREAMER_EXTRA_ID, mapobject); 208 | 209 | list_add(objects, _:tempobj); 210 | } 211 | f_close(f); 212 | 213 | if(!list_size(objects)) 214 | { 215 | printf("[MAP PARSER] %s did not contain any valid objects.", mapname); 216 | pool_remove_deep(Maps, mapindex); 217 | return INVALID_MAP_ID; 218 | } 219 | 220 | if(!list_size(buildings)) 221 | { 222 | map_str_remove_deep(data, MAP_KEY_REMOVED_BUILDING_LIST); 223 | } 224 | 225 | if(!ProcessingMaps) 226 | { 227 | RemoveNewBuildings(mapindex); 228 | } 229 | pawn_call_public("OnMapLoaded", "isi", mapindex, mapname, objects); 230 | 231 | return mapindex; 232 | } 233 | 234 | stock UnloadMap(mapid) 235 | { 236 | if(!pool_valid(Maps)) return true; 237 | if(mapid < 0 || !pool_has(Maps, mapid)) return true; 238 | 239 | new Map:data = Map:pool_get(Maps, mapid), List:objects = List:map_str_get(data, MAP_KEY_OBJECT_LIST); 240 | if(list_valid(objects) && list_size(objects)) 241 | { 242 | for(new Iter:i = list_iter(objects); iter_inside(i); iter_move_next(i)) 243 | { 244 | DestroyDynamicObject(STREAMER_TAG_OBJECT:iter_get(i)); 245 | } 246 | } 247 | 248 | pool_remove_deep(Maps, mapid); 249 | return true; 250 | } 251 | 252 | stock ReprocessMaps() 253 | { 254 | if(pool_valid(Maps) && pool_size(Maps)) 255 | { 256 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_erase_deep(i)) 257 | { 258 | new Map:data = Map:iter_get(i); 259 | new List:objects = List:map_str_get(data, MAP_KEY_OBJECT_LIST); 260 | for(new Iter:m = list_iter(objects); iter_inside(m); iter_move_next(m)) 261 | { 262 | DestroyDynamicObject(STREAMER_TAG_OBJECT:iter_get(m)); 263 | } 264 | } 265 | } 266 | 267 | ProcessMaps(); 268 | return true; 269 | } 270 | 271 | stock GetMapNameFromID(mapid) 272 | { 273 | new mapname[MAX_MAP_NAME]; 274 | if(!pool_valid(Maps) || mapid < 0 || !pool_has(Maps, mapid)) 275 | { 276 | format(mapname, sizeof(mapname), "Invalid map."); 277 | } 278 | else 279 | { 280 | new Map:data = Map:pool_get(Maps, mapid); 281 | map_str_get_str(data, MAP_KEY_NAME, mapname); 282 | } 283 | return mapname; 284 | } 285 | 286 | stock GetMapIDFromName(const name[]) 287 | { 288 | if(!pool_valid(Maps)) 289 | { 290 | return INVALID_MAP_ID; 291 | } 292 | 293 | new mapid; 294 | if(!sscanf(name, "i", mapid)) 295 | { 296 | if(mapid >= 0 && pool_has(Maps, mapid)) 297 | { 298 | return mapid; 299 | } 300 | else return INVALID_MAP_ID; 301 | } 302 | else 303 | { 304 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_move_next(i)) 305 | { 306 | new Map:data = Map:iter_get(i), mapname[MAX_MAP_NAME]; 307 | map_str_get_str(data, MAP_KEY_NAME, mapname); 308 | if(!strcmp(name, mapname, true)) 309 | { 310 | return iter_get_key(i); 311 | } 312 | } 313 | } 314 | return INVALID_MAP_ID; 315 | } 316 | 317 | stock GetMapIDFromObject(STREAMER_TAG_OBJECT:objectid) 318 | { 319 | if(!IsValidDynamicObject(objectid)) return INVALID_MAP_ID; 320 | 321 | new mapobject[2]; 322 | Streamer_GetArrayData(STREAMER_TYPE_OBJECT, objectid, E_STREAMER_EXTRA_ID, mapobject); 323 | if(mapobject[MAP_OBJ_EXTRA_OFFSET] == -5) 324 | { 325 | return mapobject[MAP_OBJ_EXTRA_MAPID]; 326 | } 327 | return INVALID_MAP_ID; 328 | } 329 | 330 | stock GetMapCount() 331 | { 332 | if(pool_valid(Maps)) 333 | { 334 | return pool_size(Maps); 335 | } 336 | else 337 | { 338 | return -1; 339 | } 340 | } 341 | 342 | stock GetMapList(String:string, bool:order = false) 343 | { 344 | if(!pool_valid(Maps)) 345 | { 346 | print("[MAP PARSER] Could not format map list because no maps are loaded."); 347 | return false; 348 | } 349 | 350 | str_append_format(string, "Slot\tMap Name\tObjects\n"); 351 | if(order) 352 | { 353 | new mapinfo[2], List:maplist = list_new(); 354 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_move_next(i)) 355 | { 356 | new Map:data = Map:iter_get(i); 357 | mapinfo[0] = iter_get_key(i); 358 | mapinfo[1] = list_size(List:map_str_get(data, MAP_KEY_OBJECT_LIST)); 359 | list_add_arr(maplist, mapinfo); 360 | } 361 | 362 | list_sort(maplist, 1, -1, true); 363 | for(new Iter:i = list_iter(maplist); iter_inside(i); iter_move_next(i)) 364 | { 365 | iter_get_arr(i, mapinfo); 366 | str_append_format(string, "%i\t%s\t%i\n", mapinfo[0], GetMapNameFromID(mapinfo[0]), mapinfo[1]); 367 | } 368 | list_delete(maplist); 369 | } 370 | else 371 | { 372 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_move_next(i)) 373 | { 374 | new Map:data = Map:iter_get(i), mapname[MAX_MAP_NAME], List:objects; 375 | map_str_get_str(data, MAP_KEY_NAME, mapname); 376 | objects = List:map_str_get(data, MAP_KEY_OBJECT_LIST); 377 | str_append_format(string, "%i\t%s\t%i\n", iter_get_key(i), mapname, list_size(objects)); 378 | } 379 | } 380 | return true; 381 | } 382 | 383 | stock IsValidMapFile(const mapname[]) 384 | { 385 | new file[256]; 386 | format(file, sizeof(file), "%s%s%s", MAP_FILE_DIRECTORY, mapname, MAP_FILE_EXTENSION); 387 | return (file_exists(file)); 388 | } 389 | 390 | stock ExportMap(mapid) 391 | { 392 | if(!pool_valid(Maps)) 393 | { 394 | printf("[MAP PARSER] Unable to export map %i due to no maps currently being loaded.", mapid); 395 | return false; 396 | } 397 | 398 | if(!pool_has(Maps, mapid)) 399 | { 400 | printf("[MAP PARSER] Unable to export map ID %i. Invalid ID.", mapid); 401 | return false; 402 | } 403 | 404 | new mapname[MAX_MAP_NAME], templine[256]; 405 | format(mapname, MAX_MAP_NAME, "%s%s", GetMapNameFromID(mapid), MAP_FILE_EXTENSION); 406 | if(file_exists(mapname)) file_delete(mapname); 407 | file_create(mapname); 408 | 409 | new STREAMER_TAG_OBJECT:tempobject, File:f = f_open(mapname, "w"), remove[REMOVE_DATA], Map:data; 410 | data = Map:pool_get(Maps, mapid); 411 | 412 | f_write(f, "new tempobject;\n"); 413 | 414 | //write removed buildings 415 | if(map_has_str_key(data, MAP_KEY_REMOVED_BUILDING_LIST)) 416 | { 417 | new List:buildings = List:map_str_get(data, MAP_KEY_REMOVED_BUILDING_LIST); 418 | for(new Iter:i = list_iter(buildings); iter_inside(i); iter_move_next(i)) 419 | { 420 | iter_get_arr(i, remove); 421 | format(templine, sizeof(templine), "RemoveBuildingForPlayer(playerid, %i, %f, %f, %f, %.2f);\n", remove[removemodel], remove[removex], remove[removey], remove[removez], remove[removeradius]); 422 | f_write(f, templine); 423 | } 424 | } 425 | 426 | //write all objects 427 | for(new k = 0; k < 2; k++) //write objects with materials first, then objects without 428 | { 429 | for(new Iter:i = list_iter(List:map_str_get(data, MAP_KEY_OBJECT_LIST)); iter_inside(i); iter_move_next(i)) 430 | { 431 | tempobject = STREAMER_TAG_OBJECT:iter_get(i); 432 | if(!IsValidDynamicObject(tempobject)) continue; 433 | 434 | new bool:matused; 435 | for(new idx = 0; idx < 16; idx++) 436 | { 437 | if(IsDynamicObjectMaterialUsed(tempobject, idx) || IsDynamicObjectMaterialTextUsed(tempobject, idx)) 438 | { 439 | matused = true; 440 | break; 441 | } 442 | } 443 | 444 | new model, Float:x, Float:y, Float:z, Float:rx, Float:ry, Float:rz, world, interior, player, Float:streamdist, Float:drawdist; 445 | GetDynamicObjectPos(tempobject, x, y, z); 446 | GetDynamicObjectRot(tempobject, rx, ry, rz); 447 | model = Streamer_GetIntData(STREAMER_TYPE_OBJECT, tempobject, E_STREAMER_MODEL_ID); 448 | world = Streamer_GetIntData(STREAMER_TYPE_OBJECT, tempobject, E_STREAMER_WORLD_ID); 449 | interior = Streamer_GetIntData(STREAMER_TYPE_OBJECT, tempobject, E_STREAMER_INTERIOR_ID); 450 | player = Streamer_GetIntData(STREAMER_TYPE_OBJECT, tempobject, E_STREAMER_PLAYER_ID); 451 | Streamer_GetFloatData(STREAMER_TYPE_OBJECT, tempobject, E_STREAMER_STREAM_DISTANCE, streamdist); 452 | Streamer_GetFloatData(STREAMER_TYPE_OBJECT, tempobject, E_STREAMER_DRAW_DISTANCE, drawdist); 453 | 454 | if(matused) 455 | { 456 | if(k != 0) continue; 457 | format(templine, sizeof(templine), "%sCreateDynamicObject(%i, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %i, %i, %i, %.2f, %.2f);\n", 458 | matused ? "tempobject = " : "", model, x, y, z, rx, ry, rz, world, interior, player, streamdist, drawdist); 459 | f_write(f, templine); 460 | for(new idx = 0; idx < 16; idx++) 461 | { 462 | if(IsDynamicObjectMaterialUsed(tempobject, idx)) 463 | { 464 | new material_model, material_txd[64], material_texture[64], material_color; 465 | GetDynamicObjectMaterial(tempobject, idx, material_model, material_txd, material_texture, material_color); 466 | 467 | format(templine, sizeof(templine), "SetDynamicObjectMaterial(tempobject, %i, %i, \"%s\", \"%s\", 0x%08x);\n", idx, material_model, material_txd, material_texture, material_color); 468 | f_write(f, templine); 469 | } 470 | if(IsDynamicObjectMaterialTextUsed(tempobject, idx)) 471 | { 472 | new material_text[256], material_size, material_font[32], material_fontsize, material_bold, material_fontcolor, material_bgcolor, material_textalign; 473 | GetDynamicObjectMaterialText(tempobject, idx, material_text, material_size, material_font, material_fontsize, material_bold, material_fontcolor, material_bgcolor, material_textalign); 474 | strreplace(material_text, "\n", "~N~"); 475 | 476 | format(templine, sizeof(templine), "SetDynamicObjectMaterialText(tempobject, %i, \"%s\", %i, \"%s\", %i, %i, 0x%08x, 0x%08x, %i);\n", 477 | idx, material_text, material_size, material_font, material_fontsize, material_bold, material_fontcolor, material_bgcolor, material_textalign); 478 | f_write(f, templine); 479 | } 480 | } 481 | } 482 | if(k == 1) 483 | { 484 | format(templine, sizeof(templine), "%sCreateDynamicObject(%i, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %i, %i, %i, %.2f, %.2f);\n", 485 | matused ? "tempobject = " : "", model, x, y, z, rx, ry, rz, world, interior, player, streamdist, drawdist); 486 | f_write(f, templine); 487 | } 488 | } 489 | } 490 | 491 | f_close(f); 492 | return true; 493 | } 494 | 495 | stock MoveMapObjects(mapid, Float:xoffset, Float:yoffset, Float:zoffset) 496 | { 497 | if(!pool_valid(Maps) || !pool_size(Maps) || mapid < 0 || !pool_has(Maps, mapid)) return false; 498 | 499 | 500 | new Map:data = Map:pool_get(Maps, mapid); 501 | for(new Iter:i = list_iter(List:map_str_get(data, MAP_KEY_OBJECT_LIST)), Float:x, Float:y, Float:z, STREAMER_TAG_OBJECT:objectid; iter_inside(i); iter_move_next(i)) 502 | { 503 | objectid = STREAMER_TAG_OBJECT:iter_get(i); 504 | if(!IsValidDynamicObject(objectid)) continue; 505 | 506 | GetDynamicObjectPos(objectid, x, y, z); 507 | SetDynamicObjectPos(objectid, x + xoffset, y + yoffset, z + zoffset); 508 | } 509 | return true; 510 | } 511 | 512 | static stock RemoveNewBuildings(mapid) 513 | { 514 | if(pool_valid(Maps) && mapid >= 0 && pool_has(Maps, mapid)) 515 | { 516 | new Map:data = Map:pool_get(Maps, mapid); 517 | if(map_has_str_key(data, MAP_KEY_REMOVED_BUILDING_LIST)) 518 | { 519 | new List:buildings, players[MAX_PLAYERS]; 520 | new playercount = GetPlayers(players); 521 | buildings = List:map_str_get(data, MAP_KEY_REMOVED_BUILDING_LIST); 522 | for(new i = 0; i < playercount; ++i) 523 | { 524 | RemoveMapBuildingsForPlayer(players[i], buildings); 525 | } 526 | } 527 | } 528 | return true; 529 | } 530 | 531 | static stock ProcessRemovedBuildings(playerid) 532 | { 533 | if(!pool_valid(Maps) || !pool_size(Maps)) return false; 534 | 535 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_move_next(i)) 536 | { 537 | new Map:data = Map:iter_get(i); 538 | if(map_has_str_key(data, MAP_KEY_REMOVED_BUILDING_LIST)) 539 | { 540 | new List:buildings = List:map_str_get(data, MAP_KEY_REMOVED_BUILDING_LIST); 541 | RemoveMapBuildingsForPlayer(playerid, buildings); 542 | } 543 | } 544 | return true; 545 | } 546 | 547 | static stock RemoveMapBuildingsForPlayer(playerid, List:buildings) 548 | { 549 | if(!list_valid(buildings)) return false; 550 | 551 | for(new Iter:i = list_iter(buildings), remove[REMOVE_DATA]; iter_inside(i); iter_move_next(i)) 552 | { 553 | iter_get_arr(i, remove); 554 | RemoveBuildingForPlayer(playerid, remove[removemodel], remove[removex], remove[removey], remove[removez], remove[removeradius]); 555 | } 556 | return true; 557 | } 558 | 559 | static stock CountMapObjects() 560 | { 561 | new count; 562 | if(pool_valid(Maps) && pool_size(Maps)) 563 | { 564 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_move_next(i)) 565 | { 566 | new Map:data = Map:iter_get(i); 567 | count += list_size(List:map_str_get(data, MAP_KEY_OBJECT_LIST)); 568 | } 569 | } 570 | return count; 571 | } 572 | 573 | static stock CountRemovedBuildings() 574 | { 575 | new count; 576 | if(pool_valid(Maps) && pool_size(Maps)) 577 | { 578 | for(new Iter:i = pool_iter(Maps); iter_inside(i); iter_move_next(i)) 579 | { 580 | new Map:data = Map:iter_get(i); 581 | if(map_has_str_key(data, MAP_KEY_REMOVED_BUILDING_LIST)) 582 | { 583 | new List:buildings = List:map_str_get(data, MAP_KEY_REMOVED_BUILDING_LIST); 584 | if(list_valid(buildings)) 585 | { 586 | count += list_size(buildings); 587 | } 588 | } 589 | } 590 | } 591 | return count; 592 | } 593 | 594 | stock IsMapParserRunning() 595 | { 596 | return ProcessingMaps; 597 | } 598 | 599 | stock SkipCurrentlyLoadingMap() 600 | { 601 | SkipMap = true; 602 | } 603 | 604 | //hooks 605 | #if defined _ALS_OnPlayerConnect 606 | #undef OnPlayerConnect 607 | #else 608 | #define _ALS_OnPlayerConnect 609 | #endif 610 | 611 | #define OnPlayerConnect MAP_OnPlayerConnect 612 | #if defined MAP_OnPlayerConnect 613 | forward MAP_OnPlayerConnect(playerid); 614 | #endif 615 | --------------------------------------------------------------------------------