I don’t have much to say at the moment, other than that there is so much potential for external tools in map making
The goal of this megathread is to develop new tools outside of Tower Unite for manipulating maps at a large scale, or doing things that couldn’t be done otherwise
NOTE: Any tools/scripts posted or shared here are not officially affiliated with PixelTail Games nor Tower Unite. They are unofficial tools created by the community for the purpose of making community content and maps
Some personal examples of external code I developed for map making:
Creating Invisible Collision Geometry
In my ttt_minecraft_b5 remake, I use canvas cones and canvas pyramids to create the collision geometry that you walk on, so that you can smoothly walk up/down blocks without having to jump (and without having to size the players up). I created a script to give all the unset canvases the invisible material, so I wouldn’t have to do it manually
- Before
- After
I didn’t want to set them invisible as I was going, because if I did there’d be no way to visualize the collision geometry. In retrospect I could have just used blocking volumes, but it would have been a bit more unwieldy (with more frustrating edge-cases)
Creating Invisible Collision Geometry: Code
This is pretty messy, but the idea is to inject CondoInvisibleMaterial into all of the CanvasWedge sections of the file:
import os.path
import struct
def main():
filename = 'CondoData'
payload = b'SurfaceMaterial\x00\x0f\x00\x00\x00ObjectProperty\x00\x42' \
+ (b'\x00' * 8) + b'\x3e\x00\x00\x00' \
+ b'/Game/Materials/CondoInvisibleMaterial.CondoInvisibleMaterial'
target = b'SurfaceMaterial\x00\x0f\x00\x00\x00ObjectProperty\x00\x09' \
+ (b'\x00' * 8) + b'\x05\x00\x00\x00' \
+ b'None'
buf = bytearray(os.path.getsize(filename))
with open(filename, 'rb') as fd:
fd.readinto(buf)
delim = b'CanvasWedge_C_'
sections = buf.split(delim)
wedges = sections[1:]
wedges = [wedge.replace(target, payload, 1) for wedge in wedges]
for x in range(len(wedges)):
idx = wedges[x].find(b'\x00')
wedges[x] = wedges[x][:(idx+1)] + b':' + wedges[x][(idx+2):]
header_delim = b'CanvasWedge'
headers = sections[0].split(header_delim)
header_payload = b'\x10\x00\x00\x00' + payload + b'\x00'
for x in range(len(headers))[1:]:
if headers[x].find(b'SurfaceMaterial') > 0:
continue
headers[x] = headers[x][:0x1d] + b'\x38\x01' + headers[x][0x1f:]
idx = headers[x].find(b'Vector') + 36
headers[x] = headers[x][:idx] + header_payload + headers[x][idx:]
new_header = header_delim.join(headers)
new_sections = [new_header] + wedges
new_buf = delim.join(new_sections)
with open('experiment_output', 'wb') as fd:
fd.write(new_buf)
if __name__ == '__main__':
main()
It’s straight-forward to do the same for CanvasPyramid, replacing “CanvasCone” with “CanvasPyramid” and replacing some size bytes.
Resizing Cones
In LC Deathrun, I wanted to resize these cones but didn’t feel confident about group-scaling them. Instead I wrote a script to replace the bytes corresponding to world scale in the .map file
-
Before:
-
After:
-
Code:
Resizing Cones: Code
import os.path
import struct
import re
def scaleset(match):
floats = match.group(4)
scale_x, scale_y, scale_z = struct.unpack('<fff', floats)
scale_z *= 2
out_bytes = struct.pack('<fff', scale_x, scale_y, scale_z)
return b''.join(match.groups()[:-1]) + out_bytes
def main():
filename = 'CondoData'
buf = bytearray(os.path.getsize(filename))
with open(filename, 'rb') as fd:
fd.readinto(buf)
buf = re.sub(br'(CanvasCone)(.+?)(WorldScale.+?Vector\x00{18})(.{12})', scaleset, buf)
with open('experiment_output', 'wb') as fd:
fd.write(buf)
if __name__ == '__main__':
main()
Discord server link: Tower Unite Tooling Developers