2024-07-31 23:54:35 -03:00
|
|
|
#!/bin/env python
|
|
|
|
# cookcollision.py
|
|
|
|
# Cook 16x16 tile collision from Tiled tile data.
|
|
|
|
# Make sure you exported a 16x16 tile with proper collision data.
|
|
|
|
|
|
|
|
import json
|
|
|
|
import sys
|
2024-08-12 22:28:34 -03:00
|
|
|
import numpy as np
|
|
|
|
import math
|
|
|
|
from ctypes import c_ushort, c_ubyte, c_int32
|
2024-07-31 23:54:35 -03:00
|
|
|
from enum import Enum
|
|
|
|
from pprint import pp as pprint
|
|
|
|
from math import sqrt
|
|
|
|
|
2024-08-01 18:22:15 -03:00
|
|
|
# Ensure binary C types are encoded as big endian
|
2024-07-31 23:54:35 -03:00
|
|
|
c_ushort = c_ushort.__ctype_be__
|
2024-08-12 22:28:34 -03:00
|
|
|
c_int32 = c_int32.__ctype_be__
|
2024-07-31 23:54:35 -03:00
|
|
|
|
2024-08-01 18:22:15 -03:00
|
|
|
# This package depends on shapely because I'm fed up with attempting to code
|
|
|
|
# point and polygon checking myself. On arch linux, install python-shapely.
|
|
|
|
from shapely.geometry import Point
|
|
|
|
from shapely.geometry.polygon import Polygon
|
|
|
|
|
2024-07-31 23:54:35 -03:00
|
|
|
|
|
|
|
class Direction(Enum):
|
|
|
|
DOWN = 0
|
|
|
|
UP = 1
|
|
|
|
LEFT = 2
|
|
|
|
RIGHT = 3
|
|
|
|
|
|
|
|
|
2024-08-01 18:25:45 -03:00
|
|
|
def point_in_geometry(p, points):
|
2024-08-01 18:22:15 -03:00
|
|
|
point = Point(p[0], p[1])
|
|
|
|
shape = Polygon(points)
|
|
|
|
return shape.contains(point) or shape.intersects(point)
|
2024-07-31 23:54:35 -03:00
|
|
|
|
|
|
|
|
2024-08-12 22:28:34 -03:00
|
|
|
def normalize(v):
|
|
|
|
norm = np.linalg.norm(v)
|
|
|
|
return [c / norm for c in v]
|
|
|
|
|
|
|
|
|
|
|
|
def fix_angle(x):
|
|
|
|
# This ensures that an angle in radians is always on their
|
|
|
|
# 1st or 4th quadrant equivalent, and also on the first lap.
|
|
|
|
fixed = x
|
|
|
|
if (x >= (np.pi / 2)) and (x < np.pi):
|
|
|
|
fixed = (2 * np.pi) - (np.pi - x)
|
|
|
|
if (x >= np.pi) and (x < (1.5 * np.pi)):
|
|
|
|
fixed = x - np.pi
|
|
|
|
return fixed % (2 * np.pi)
|
|
|
|
|
|
|
|
|
|
|
|
def to_psx_angle(a):
|
|
|
|
# PSX angles are given in degrees, ranged from 0.0 to 1.0 in 20.12
|
|
|
|
# fixed-point format (therefore from 0 to 4096).
|
|
|
|
# All we need to do is fix its quadrant and lap, convert it to a
|
|
|
|
# ratio [0..360], then multiply it by 4096. This is how we get
|
|
|
|
# our angle.
|
|
|
|
# Final gsp->(xsp, ysp) conversions in-game are given as
|
|
|
|
# {x: (gsp * cos(x) >> 12), y: (gsp * -sin(x)) >> 12}.
|
|
|
|
a = np.rad2deg(fix_angle(a))
|
|
|
|
rat = a / 360
|
|
|
|
return math.floor(rat * 4096)
|
|
|
|
|
|
|
|
|
2024-08-01 18:25:45 -03:00
|
|
|
def get_height_mask(d: Direction, points):
|
2024-07-31 23:54:35 -03:00
|
|
|
# Perform iterative linecast.
|
|
|
|
# Linecast checks for a point within a geometry starting at a height
|
|
|
|
# of 15 until 1 (inclusive). 0 means no collision at that height.
|
|
|
|
# We do that for each X spot on our geometry.
|
|
|
|
# Of course, if pointing downwards, we go from left to right, top to bottom.
|
|
|
|
# If using any other direction... flip it accordingly.
|
|
|
|
heightmask = []
|
2024-08-12 22:28:34 -03:00
|
|
|
angle = 0
|
2024-07-31 23:54:35 -03:00
|
|
|
for pos in range(16):
|
|
|
|
found = False
|
|
|
|
for height in reversed(range(1, 16)):
|
|
|
|
if d == Direction.DOWN:
|
|
|
|
x = pos
|
|
|
|
y = 16 - height
|
|
|
|
elif d == Direction.UP:
|
|
|
|
x = 15 - pos
|
|
|
|
y = height
|
|
|
|
elif d == Direction.LEFT:
|
|
|
|
x = height
|
|
|
|
y = pos
|
|
|
|
elif d == Direction.RIGHT:
|
|
|
|
x = 16 - height
|
|
|
|
y = 15 - pos
|
|
|
|
|
2024-08-01 18:25:45 -03:00
|
|
|
if point_in_geometry([x, y], points):
|
2024-07-31 23:54:35 -03:00
|
|
|
found = True
|
|
|
|
heightmask.append(height)
|
|
|
|
break
|
|
|
|
if not found:
|
|
|
|
heightmask.append(0)
|
2024-08-12 22:28:34 -03:00
|
|
|
|
|
|
|
# Build vector according to direction
|
|
|
|
# and heightmask
|
|
|
|
# TODO: Maybe the referential dirvec is wrong?
|
|
|
|
vector = [0, 0]
|
|
|
|
dirvec = [0, 0]
|
|
|
|
dirv = 0
|
|
|
|
delta = heightmask[0] - heightmask[-1]
|
|
|
|
if d == Direction.DOWN:
|
|
|
|
# Vector points left to right
|
|
|
|
vector = [16, delta]
|
|
|
|
dirvec = [1, 0]
|
|
|
|
dirv = 0
|
|
|
|
elif d == Direction.UP:
|
|
|
|
# Vector points right to left
|
|
|
|
vector = [-16, -delta]
|
|
|
|
dirvec = [-1, 0]
|
|
|
|
dirv = 0
|
|
|
|
elif d == Direction.LEFT:
|
|
|
|
# Vector points top to bottom
|
|
|
|
vector = [-delta, 16]
|
|
|
|
dirvec = [0, 1]
|
|
|
|
dirv = 1
|
|
|
|
elif d == Direction.RIGHT:
|
|
|
|
# Vector points bottom to top
|
|
|
|
vector = [delta, -16]
|
|
|
|
dirvec = [0, -1]
|
|
|
|
dirv = 1
|
|
|
|
|
|
|
|
vector = normalize(vector)
|
|
|
|
angle = math.atan2(dirvec[1], dirvec[0]) - math.atan2(vector[1], vector[0])
|
|
|
|
angle = to_psx_angle(angle)
|
|
|
|
return (heightmask, angle)
|
2024-07-31 23:54:35 -03:00
|
|
|
|
|
|
|
|
|
|
|
def parse_masks(tiles):
|
|
|
|
res = []
|
|
|
|
for tile in tiles:
|
|
|
|
points = tile.get("points")
|
|
|
|
id = tile.get("id")
|
2024-08-12 22:28:34 -03:00
|
|
|
(floor, floor_angle) = get_height_mask(Direction.DOWN, points)
|
|
|
|
(ceil, ceil_angle) = get_height_mask(Direction.UP, points)
|
|
|
|
(rwall, rwall_angle) = get_height_mask(Direction.RIGHT, points)
|
|
|
|
(lwall, lwall_angle) = get_height_mask(Direction.LEFT, points)
|
|
|
|
|
2024-07-31 23:54:35 -03:00
|
|
|
res.append(
|
|
|
|
{
|
|
|
|
"id": tile.get("id"),
|
|
|
|
"masks": {
|
2024-08-12 22:28:34 -03:00
|
|
|
"floor": [floor_angle, floor],
|
|
|
|
"ceiling": [ceil_angle, ceil],
|
|
|
|
"rwall": [rwall_angle, rwall],
|
|
|
|
"lwall": [lwall_angle, lwall],
|
2024-07-31 23:54:35 -03:00
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
def load_json(filename):
|
|
|
|
with open(filename) as fp:
|
|
|
|
return json.load(fp)
|
|
|
|
|
|
|
|
|
|
|
|
def parse_json(j):
|
|
|
|
tiles = j.get("tiles")
|
|
|
|
res = []
|
|
|
|
for tile in tiles:
|
|
|
|
grp = tile.get("objectgroup")
|
|
|
|
if grp:
|
|
|
|
objs = grp.get("objects")
|
|
|
|
if objs:
|
|
|
|
o = objs[0]
|
|
|
|
id = tile.get("id")
|
|
|
|
x = round(o.get("x"), 0)
|
|
|
|
y = round(o.get("y"), 0)
|
|
|
|
if o.get("polygon"):
|
2024-08-08 01:29:14 -03:00
|
|
|
# Could be a triangle or a quad,
|
|
|
|
# but could also be anything, in fact.
|
2024-07-31 23:54:35 -03:00
|
|
|
vertices = o.get("polygon")
|
2024-08-08 01:29:14 -03:00
|
|
|
points = []
|
|
|
|
for vertex in vertices:
|
|
|
|
points.append(
|
|
|
|
[round(vertex.get("x") + x), round(vertex.get("y") + y)]
|
2024-08-01 01:26:40 -03:00
|
|
|
)
|
2024-08-08 01:29:14 -03:00
|
|
|
res.append(
|
|
|
|
{
|
|
|
|
"id": id,
|
|
|
|
"points": points,
|
|
|
|
}
|
|
|
|
)
|
2024-07-31 23:54:35 -03:00
|
|
|
else:
|
|
|
|
# Treat as quad
|
|
|
|
width = round(o.get("width"), 0)
|
|
|
|
height = round(o.get("height"), 0)
|
|
|
|
points = [
|
|
|
|
# xy0
|
|
|
|
[x, y],
|
|
|
|
# xy1
|
|
|
|
[x + width, y],
|
|
|
|
# xy3
|
|
|
|
[x + width, x + height],
|
2024-08-01 18:22:15 -03:00
|
|
|
# xy2
|
|
|
|
[x, y + height],
|
2024-07-31 23:54:35 -03:00
|
|
|
]
|
|
|
|
res.append(
|
|
|
|
{
|
|
|
|
"id": id,
|
|
|
|
"points": points,
|
|
|
|
}
|
|
|
|
)
|
2024-08-18 03:01:52 -03:00
|
|
|
# print(f"Number of collidable tiles: {len(res)}")
|
2024-07-31 23:54:35 -03:00
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
def write_mask_data(f, mask_data):
|
|
|
|
# Join mask data. We have 16 heights; turn them into 8 bytes
|
|
|
|
data = []
|
|
|
|
for i in range(0, 16, 2):
|
|
|
|
a = (mask_data[i] & 0x0F) << 4
|
|
|
|
b = mask_data[i + 1] & 0x0F
|
|
|
|
data.append(c_ubyte(a | b))
|
|
|
|
for b in data:
|
|
|
|
f.write(b)
|
|
|
|
|
|
|
|
|
|
|
|
# Binary layout:
|
|
|
|
# 1. Number of tiles (ushort, 2 bytes)
|
2024-08-12 22:28:34 -03:00
|
|
|
# 2. Tile data [many]
|
2024-07-31 23:54:35 -03:00
|
|
|
# 2.1. tile id (ushort, 2 bytes)
|
2024-08-12 22:28:34 -03:00
|
|
|
# 2.2. Floor
|
|
|
|
# 2.2.1. Angle (4 bytes - PSX format)
|
|
|
|
# 2.2.2. Data (8 bytes)
|
|
|
|
# 2.3. Right wall
|
|
|
|
# 2.3.1. Angle (4 bytes - PSX format)
|
|
|
|
# 2.3.2. Data (8 bytes)
|
|
|
|
# 2.4. Ceiling
|
|
|
|
# 2.4.1. Angle (4 bytes - PSX format)
|
|
|
|
# 2.4.2. Data (8 bytes)
|
|
|
|
# 2.5. Left wall
|
|
|
|
# 2.5.1. Angle (4 bytes - PSX format)
|
|
|
|
# 2.5.2. Data (8 bytes)
|
2024-07-31 23:54:35 -03:00
|
|
|
def write_file(f, tile_data):
|
|
|
|
f.write(c_ushort(len(tile_data)))
|
|
|
|
for tile in tile_data:
|
|
|
|
f.write(c_ushort(tile.get("id")))
|
2024-08-12 22:28:34 -03:00
|
|
|
masks = tile.get("masks")
|
|
|
|
f.write(c_int32(masks.get("floor")[0]))
|
|
|
|
write_mask_data(f, masks.get("floor")[1])
|
|
|
|
f.write(c_int32(masks.get("rwall")[0]))
|
|
|
|
write_mask_data(f, masks.get("rwall")[1])
|
|
|
|
f.write(c_int32(masks.get("ceiling")[0]))
|
|
|
|
write_mask_data(f, masks.get("ceiling")[1])
|
|
|
|
f.write(c_int32(masks.get("lwall")[0]))
|
|
|
|
write_mask_data(f, masks.get("lwall")[1])
|
2024-07-31 23:54:35 -03:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
jsonfile = sys.argv[1]
|
|
|
|
outfile = sys.argv[2]
|
|
|
|
j = load_json(jsonfile)
|
|
|
|
parsed = parse_json(j)
|
|
|
|
masks = parse_masks(parsed)
|
2024-08-01 18:22:15 -03:00
|
|
|
# pprint(list(filter(lambda x: x.get("id") == 1, masks))[0])
|
2024-07-31 23:54:35 -03:00
|
|
|
with open(outfile, "wb") as f:
|
|
|
|
write_file(f, masks)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|