/
vertalert.py
262 lines (216 loc) · 9.14 KB
/
vertalert.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env python
#
# Copyright (c) 2012, 2013 Robert Martens <robert.martens@gmail.com>
# See file COPYING.txt for MIT license terms.
"""
Find floating point vertex coordinates in a Source engine .vmf file.
Looks through a .vmf for any brush planes whose vertices have
non-integer coordinates, printing a list to stdout and optionally
writing out a new file with rounded values. As a side effect of its
brute force regex search, vertalert only checks enabled VisGroups.
"""
import decimal
import os
import re
import sys
def get_dev(coord, snap):
orig = decimal.Decimal(coord)
rounded = (orig / snap).quantize(1, decimal.ROUND_HALF_EVEN) * snap
return max(rounded, orig) - min(rounded, orig)
def get_max_dev(planes, snap):
"""
Search a brush's planes for the largest deviation any of its
vertices' coordinates make from the nearest integer.
Keyword arguments:
planes: list of strings representing the planes to check
"""
floats = []
for plane in planes:
# We're only looking for coordinates that were written to the
# VMF as floating point values, perhaps in scientific notation.
floats += re.findall(r'-?\d+\.\d+e?-?\d*', plane)
devs = []
for coord in floats:
devs.append(get_dev(coord, snap))
return max(devs)
def fix_plane(plane, regex, thresh, snaplo, snaphi):
"""
Use 'regex' pattern to find floating point coordinates in 'plane',
round to nearest integer, and return corrected plane string.
Keyword arguments:
plane: string to search for floats
regex: Regular Expression pattern to use for search
"""
floats = re.findall(regex, plane)
plane_new = plane
for coord in floats:
orig = decimal.Decimal(coord)
if get_dev(orig, snaplo) < thresh:
rounded = (orig / snaplo).quantize(1, decimal.ROUND_HALF_EVEN) * snaplo
rounded = rounded.normalize()
# I replace str(coord) instead of orig here, since
# that would miss values using scientific notation.
plane_new = plane_new.replace(str(coord), str(rounded), 1)
elif snaphi is not None:
rounded = (orig / snaphi).quantize(1, decimal.ROUND_HALF_EVEN) * snaphi
rounded = rounded.normalize()
plane_new = plane_new.replace(str(coord), str(rounded), 1)
return plane_new
def fix_brushes(brushes, thresh, vmf_in, snaplo, snaphi):
"""
Find and fix brushes with floating point plane vertex coordinates.
Returns a tuple containing the total number of brushes with floats,
a list of the greatest deviation any of a brush's coordinates makes
from the nearest integer, and a fixed version of vmf_in. Corrects
only deviations less than 'thresh'.
Keyword arguments:
brushes: list of brush strings to search
thresh: threshold below which to ignore/round coordinates
vmf_in: string containing input VMF contents
"""
vmf_out = vmf_in
rounded_count = 0
percent = len(brushes) / 100.0
suspects = []
for i, brush in enumerate(brushes):
brush_id = int(re.search(r'"id"\s"(\d+)"', brush).group(1))
float_planes = []
for plane in re.findall(r'"plane"\s".*?"', brush, re.DOTALL):
if '.' in plane:
float_planes.append(plane)
if not float_planes:
continue
brush_new = brush
for plane in float_planes:
plane_new = fix_plane(plane, r'-?\d+\.\d+e?-?\d*', thresh, snaplo, snaphi)
brush_new = brush_new.replace(plane, plane_new)
vmf_out = vmf_out.replace(brush, brush_new)
max_dev = get_max_dev(float_planes, snaplo)
if max_dev < thresh or snaphi is not None:
rounded_count += 1
else:
suspects.append((brush_id, max_dev))
sys.stdout.write('\r%s%% complete' % str(int(i / percent)))
sys.stdout.flush()
sys.stdout.write("\r \n")
sys.stdout.flush()
return (rounded_count, suspects, vmf_out)
def print_dev_table(suspects, rounded_count, fix):
"""
Print, to stdout, a table displaying each brush ID and its
coordinates' maximum deviation from the nearest integer.
Keyword arguments:
sorted_devs: a list of tuples pairing brush id with max deviation
"""
if suspects:
suspects = sorted(suspects, key=lambda suspect: suspect[-1])
max_id_width = len(str(max(suspects[0])))
left_w = max(max_id_width, len("Suspect id"))
header = ("Suspect ID").rjust(left_w) + ' ' + "Max dev" + '\n'
sys.stdout.write(header)
sys.stdout.write('-' * (len(header) - 1) + '\n')
for suspect in suspects:
suspect_str = str(suspect[0]).rjust(left_w) + " " + str(suspect[1]) + "\n"
sys.stdout.write(suspect_str)
sys.stdout.flush()
sys.stdout.write('\n')
if len(suspects) == 1:
warn_suffix = ""
else:
warn_suffix = "es"
if rounded_count == 1:
action_suffix = ""
else:
action_suffix = "es"
if fix:
action = " automatically rounded"
else:
action = " ignored"
sys.stdout.write(str(rounded_count) + " brush" +
action_suffix + action + '\n')
sys.stdout.write(str(len(suspects)) + " suspect brush" +
warn_suffix + " remaining\n")
sys.stdout.flush()
def vertalert(file_in, fix=False, fixname=None, thresh=None,
snaplo=None, snaphi=None):
"""
Find, and optionally fix, floating point plane coordinates in a
Source engine .vmf file.
Prints a list to stdout of all values greater than or equal to
thresh, rounds (when using --fix) or ignores all other values.
Keyword arguments:
file_in: .vmf file to check
fix: write out a new file with rounded coordinates (default False)
fixname: filename when using --fix (default appends _VERTALERT)
thresh: threshold below which to ignore/round coordinates (default 0.2)
Please note this function currently only checks enabled VisGroups.
"""
if not os.path.exists(file_in):
sys.stderr.write("Could not find " + file_in + "!\n")
return -1
if os.path.splitext(file_in)[1] != ".vmf":
sys.stderr.write("Input must be a .vmf file!\n")
return -1
if fixname is None:
in_name_split = os.path.splitext(file_in)
fixname = in_name_split[0] + "_VERTALERT" + in_name_split[1]
if snaplo is None:
snaplo = decimal.Decimal('1')
if thresh is None:
thresh = snaplo * decimal.Decimal('0.2')
with open(file_in, 'r') as vmf:
vmf_in = vmf.read()
# I no longer read the input in universal line ending mode above,
# as that changes the type of line endings written to the output
# when using --fix. Although Hammer can deal with at least Windows
# and Unix endings, I prefer to write out the same thing I read in.
# The \r?\n in this regular expression became necessary to ensure
# Windows, Linux and OS X worked the same way. The pattern is that
# of the beginning of a solid entry, whose VisGroup is enabled, and
# breaks down as follows:
#
# solid - The word 'solid'.
# \r?\n - Zero or one carriage return followed by one newline. In
# Windows, \n is what \r\n is under Linux and OS X.
# \t{ - Tab, open curly brace
# .*? - Zero or more of any character (including newlines, thanks
# to my use of re.DOTALL in the call to findall), non-greedy.
# \r?\n\t} - Zero or one carriage return, one newline, one tab,
# closing curly brace.
brushes = re.findall(r'solid\r?\n\t{.*?\r?\n\t}', vmf_in, re.DOTALL)
rounded_count, suspects, vmf_out = fix_brushes(brushes, thresh, vmf_in,
snaplo, snaphi)
print_dev_table(suspects, rounded_count, fix)
if fix:
with open(fixname, 'w') as vmf:
vmf.write(vmf_out)
sys.stdout.write("\nWrote " + fixname + "!\n")
sys.stdout.flush()
return 0
if __name__ == '__main__':
import argparse
PARSER = argparse.ArgumentParser(description="VertAlert 0.2.1")
PARSER.add_argument("input", help="VMF to check")
PARSER.add_argument(
"-f", "--fix", help="write fixed up VMF", action="store_true")
PARSER.add_argument(
"-fn", "--fixname",
help="filename to use with --fix (default appends _VERTALERT)")
PARSER.add_argument(
"-t", "--thresh",
type=decimal.Decimal,
help="threshold below which to ignore/round coordinates "
"(default snaplo * 0.2)")
PARSER.add_argument(
"-sl", "--snaplo",
type=decimal.Decimal,
help="coordinates with deviations less than thresh will be rounded to "
"the nearest multiple of this value (default 1)")
PARSER.add_argument(
"-sh", "--snaphi",
type=decimal.Decimal,
help="coordinates with deviations equal to or greater than thresh will "
"be rounded to the nearest multiple of this value (default None)")
ARGS = PARSER.parse_args()
vertalert(ARGS.input, ARGS.fix, ARGS.fixname, ARGS.thresh,
ARGS.snaplo, ARGS.snaphi)