├── README.md ├── LICENSE.md └── main.py /README.md: -------------------------------------------------------------------------------- 1 | ![Result](https://i.imgur.com/zajutDR.png) 2 | ![CMD window](https://i.imgur.com/vpLD6jx.png) 3 | 4 | Triggify semi-automatically places teleport triggers above all the surfaces marked with a certain texture, it struggles with certain complex shapes, Those need to be manually deleted. 5 | 6 | Some of the faces on the triggers are bugged you can fix it by going into 'Map > Check for problems: 'Fix all (of type). 7 | 8 | All triggers are placed into a seperate map called "generated_triggers.vmf" in the same folder as the original map. 9 | 10 | Enjoy, GorangeNinja 11 | 12 | If there's a problem, send me a message on Discord: GorangeNinja#6433 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GorangeNinja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | from itertools import chain 4 | from time import sleep 5 | 6 | 7 | # This function converts string ints into ints, and string floats into floats (.vmf's use both ints and floats for coordinates) 8 | def num(s): 9 | try: 10 | return int(s) 11 | except ValueError: 12 | return float(s) 13 | 14 | 15 | class Vertex: 16 | height = 2 # Trigger_teleport's size on the Z axis 17 | 18 | def __init__(self, x, y, z): 19 | self.x = x 20 | self.y = y 21 | self.z = z 22 | 23 | self.fz = z # fz represents the fixed z coordinate that gets exported (in generated_triggers.vmf) 24 | 25 | def compare(self, other): 26 | #print(self.x, self.y, other.x, other.y) 27 | 28 | # This script works by taking a shape and moving the vertices on the top face up 2 units, and the vertices on the bottom 29 | # face are moved up to where the top face vertices were. 30 | # To figure out when to add 2 to the height (z) or when to move it up to the top face vertex, we compare 2 vertices' x and y 31 | # coordinates, if they're equal we can then compare their z coordinate, if it's bigger we add 2, if it's lower we 32 | # give it the other vertex's z coordinate. If they're equal we can just ignore it (some vertices are shared by several faces) 33 | # All the z coordinate manipulation is done to fz, which is only used at the end when exported to a file. 34 | if self.x == other.x and self.y == other.y: 35 | if self.z < other.z: 36 | self.fz = other.z 37 | #print("HERE:", self.fz, self.z, other.z) 38 | return True 39 | 40 | elif self.z > other.z: 41 | self.fz += self.height 42 | return True 43 | 44 | # This is used when generating "generated_triggers.vmf" 45 | def return_fixed_z(self): 46 | return self.x, self.y, self.fz 47 | 48 | 49 | class Face: 50 | id_counter = 1 # vmf's use id's for faces, no idea how they work, this might not even be necessary. 51 | 52 | def __init__(self, v1: Vertex, v2: Vertex, v3: Vertex): 53 | self.v1 = v1 54 | self.v2 = v2 55 | self.v3 = v3 56 | 57 | def return_all(self): 58 | return self.v1, self.v2, self.v3 59 | 60 | def generate_face(self): 61 | print(*self.v1.return_fixed_z()) 62 | Face.id_counter += 1 63 | return ''' 64 | side 65 | {{ 66 | "id" "{9}" 67 | "plane" "({0} {1} {2}) ({3} {4} {5}) ({6} {7} {8})" 68 | "material" "TOOLS/TOOLSTRIGGER" 69 | "uaxis" "[1 0 0 0] 0.5" 70 | "vaxis" "[0 -1 0 0] 0.5" 71 | "rotation" "0" 72 | "lightmapscale" "128" 73 | "smoothing_groups" "0" 74 | }}'''.format(*self.v1.return_fixed_z(), *self.v2.return_fixed_z(), *self.v3.return_fixed_z(), self.id_counter) 75 | 76 | 77 | 78 | class Shape: 79 | id_counter = 1 # Same comment as for face's id_counter 80 | teleport_target = "default" 81 | use_landmark_angles = True 82 | csgo = 0 # 0 is csgo, 1 is css 83 | 84 | def __init__(self, *args: Face): 85 | self.faces = [] # Shapes are not limited to 6 faces, they can be more or less 86 | self.faces.extend(args) 87 | 88 | def get_all_vertices(self): 89 | # This returns every single vertex on this shape (a normal cube has (number of faces * 3 = 6 * 3 = 18) vertices) 90 | return list(chain.from_iterable([f.return_all() for f in self.faces])) 91 | 92 | def check(self): 93 | all_vertices = self.get_all_vertices() 94 | 95 | # Here we compare every single vertex on a shape against all other vertices, some vertices are repeated but it doesn't 96 | # matter because it's really fast anyways (see Vertex.compare function for more information) 97 | for v in all_vertices: 98 | for v2 in all_vertices: 99 | if v.compare(v2): 100 | # If the compare function fixes the z coordinate, we don't need to keep comparing so we skip to next vertex 101 | break 102 | 103 | # If the vertex didn't find any matches (aka rotated solids, etc...) we just add 2 to the z axis and the 104 | # user has to manually fix it (if they want to, it's still usable just not as pretty) 105 | # If all vertices of a solid are on unique x and y coordinates, the entire shape is essentially just moved 106 | # up 2 units 107 | if v.z == v.fz: 108 | v.fz += 2 109 | 110 | # This bad boy mimics how vmf's work, it's ugly as sin, but what can you do without an API? 111 | def generate_shape(self): 112 | s = ''' 113 | entity 114 | {{ 115 | "id" "{0}" 116 | "classname" "trigger_teleport"'''.format(self.id_counter) 117 | if self.csgo == 0: 118 | s += ''' 119 | "CheckDestIfClearForPlayer" "0"''' 120 | 121 | s += ''' 122 | "origin" "{0} {1} {2}" 123 | '''.format(*self.faces[0].v1.return_fixed_z()) 124 | 125 | if self.csgo == 0: 126 | s += ' "spawnflags" "4097"' 127 | 128 | elif self.use_landmark_angles: 129 | s += ' "spawnflags" "33"' 130 | 131 | else: 132 | s += ' "spawnflags" "1"' 133 | 134 | s += ''' 135 | "StartDisabled" "0" 136 | "target" "{0}"'''.format(self.teleport_target) 137 | 138 | if self.csgo == 0: 139 | s += ''' 140 | "UseLandmarkAngles" "{0}"'''.format(int(self.use_landmark_angles)) 141 | 142 | s += ''' 143 | solid 144 | {{ 145 | "id" "{0}"'''.format(self.id_counter) 146 | Shape.id_counter += 1 147 | 148 | for f in self.faces: 149 | s += f.generate_face() 150 | 151 | s += ''' 152 | editor 153 | { 154 | "color" "0 136 109" 155 | "visgroupshown" "1" 156 | "visgroupautoshown" "1" 157 | } 158 | } 159 | }''' 160 | 161 | return s 162 | 163 | print("Gorange's Triggify v1.1") 164 | print("------------------------------") 165 | print("Put the texture on all the floors you want trigger zones on") 166 | print("Some of the faces on the triggers are buggy you can fix it by", "going into 'Map > Check for problems: 'Fix all (of type)'") 167 | print("Output can be found in 'generated_triggers.vmf'") 168 | print("It sometimes produces invalid shapes, just delete them") 169 | print("Enjoy the time you've saved!") 170 | print("------------------------------") 171 | try: 172 | print("Selected map: " + str(sys.argv[1])) 173 | except IndexError: 174 | print("No map selected") 175 | print("Please drag a .vmf onto this .exe") 176 | sleep(4) 177 | quit() 178 | print("------------------------------") 179 | 180 | # Pretty ugly try/except block here, but in case of a big crash it could help me find a solution 181 | try: 182 | indent = 20 # Indent represents the current level of indentation in the .vmf at that current line 183 | found = False # If it found a face with the texture we're looking for 184 | Shape.csgo = int(input("CS:GO or CS:S (0 or 1): ")) 185 | Vertex.height = int(input("Trigger height (2 is recommended): ")) 186 | match = input("Texture name (ex: ADS/AD01): ").upper() # In vmf's all texture names are capitalized, don't know why 187 | Shape.teleport_target = input("Remote destination: ") 188 | Shape.use_landmark_angles = int(input("Use landmark angles (0 or 1): ")) 189 | shape_list = [] 190 | face_list = [] 191 | 192 | # Here we open the file that was dragged onto the script/executable 193 | with open(sys.argv[1], "r") as vmf: 194 | # We painstakingly go through every single line of the .vmf 195 | for line in vmf.readlines(): 196 | if "solid" in line: 197 | # This indent variable keeps track of opening and closing curly braces, once this gets back to 0 198 | # we know we've read the entire solid 199 | indent = 0 200 | continue 201 | 202 | if '"plane"' in line: 203 | # Every face has 3 vertices, these are extracted below 204 | vert_list = [] 205 | # This isn't very pretty but it gets the job of turning a string into usable data done 206 | l = re.findall(r'\(.*?\)', line) 207 | for vert in l: 208 | str_vert = vert[1:-1] 209 | vert_list.append(Vertex(*[num(n) for n in str_vert.split(" ")])) 210 | 211 | face_list.append(Face(*vert_list)) 212 | 213 | # If we find the texture we're looking for 214 | if match in line: 215 | found = True 216 | 217 | if "{" in line: 218 | indent += 1 219 | 220 | if "}" in line: 221 | indent -= 1 222 | 223 | # Once everything in the solid has been read 224 | if not indent: 225 | # Once a solid shows up in the .vmf we keep track of every face of that solid, if we find the texture, 226 | # we store every face previously extracted, otherwise we just reset and start all over again 227 | if found: 228 | shape_list.append(Shape(*face_list)) 229 | face_list = [] 230 | found = False 231 | # When reading a solid, indent usually fluctuates by 3, here we're just making sure it's not going 232 | # to accidentally trigger "if not indent" a couple lines above 233 | indent = 20 234 | face_list = [] 235 | 236 | # Here's where it calls the function to calculate all the fixed z coordinates 237 | for shape in shape_list: 238 | shape.check() 239 | 240 | # This beauty is all the other stuff that goes into a fresh .vmf, we add all the generated trigger_teleports at the end 241 | # of this string 242 | t = '''versioninfo 243 | { 244 | "editorversion" "400" 245 | "editorbuild" "8075" 246 | "mapversion" "0" 247 | "formatversion" "100" 248 | "prefab" "0" 249 | } 250 | viewsettings 251 | { 252 | "bSnapToGrid" "1" 253 | "bShowGrid" "1" 254 | "bShowLogicalGrid" "0" 255 | "nGridSpacing" "64" 256 | "bShow3DGrid" "0" 257 | } 258 | world 259 | { 260 | "id" "1" 261 | "mapversion" "0" 262 | "classname" "worldspawn" 263 | "skyname" "sky_dust" 264 | "maxpropscreenwidth" "-1" 265 | "detailvbsp" "detail.vbsp" 266 | "detailmaterial" "detail/detailsprites" 267 | } 268 | cameras 269 | { 270 | "activecamera" "-1" 271 | } 272 | cordons 273 | { 274 | "active" "0" 275 | }''' 276 | 277 | 278 | with open("generated_triggers.vmf", "w+") as gen: 279 | gen.writelines(t) 280 | for shape in shape_list: 281 | # Here we generate the text that .vmf files can read 282 | gen.writelines(shape.generate_shape()) 283 | 284 | print("-------------------") 285 | print("Done") 286 | 287 | sleep(2) 288 | 289 | # Pretty bad error logging system, but whatever 290 | except Exception as e: 291 | with open("crashlog.txt", "a+") as log: 292 | log.write(str(e) + "\n" + "-----------------------") 293 | 294 | 295 | 296 | print("ERROR: See crash log") 297 | sleep(2) 298 | --------------------------------------------------------------------------------