303 lines
220 KiB
Text
303 lines
220 KiB
Text
|
{
|
||
|
"cells": [
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 1,
|
||
|
"id": "3bb5cd79-48af-47ba-b0c7-6ddb645fdead",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"import numpy as np\n",
|
||
|
"import struct\n",
|
||
|
"from memorylib import Dolphin\n",
|
||
|
"import matplotlib.pyplot as plt\n",
|
||
|
"from matplotlib import patches\n",
|
||
|
"import itertools\n",
|
||
|
"from collections import defaultdict, Counter\n",
|
||
|
"from shape import Polygon, Polyhedron\n",
|
||
|
"array = np.array\n",
|
||
|
"normalize = lambda x: x/np.linalg.norm(x)\n",
|
||
|
"read_struct = lambda addr, fmt: struct.unpack(fmt, d.read_ram(addr-0x80000000, struct.calcsize(fmt)))\n",
|
||
|
"\n",
|
||
|
"def hook():\n",
|
||
|
" global d, dolphin\n",
|
||
|
" d = dolphin = Dolphin()\n",
|
||
|
" assert dolphin.find_dolphin(), 'Dolphin not found'\n",
|
||
|
" assert dolphin.init_shared_memory(), 'MEM1 not found'\n",
|
||
|
" assert dolphin.read_ram(0, 3).tobytes() == b'GMS', 'Current game is not Sunshine'"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 2,
|
||
|
"id": "b2dd3225-7f18-45b8-87f6-bb42279fb2e7",
|
||
|
"metadata": {},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"name": "stdout",
|
||
|
"output_type": "stream",
|
||
|
"text": [
|
||
|
"1868634166512 0x1b3134694f0\n"
|
||
|
]
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"hook()"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "194bce4c-80df-41ab-afa5-90616c2b0ca3",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"## Classes and Functions"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 3,
|
||
|
"id": "2f71120c-696f-4eec-b7cc-10f30f1547d1",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"class Surface:\n",
|
||
|
" def __init__(self, surtype, surpara, trntype, unk7, verts, vidxs=None, n=None, c=None):\n",
|
||
|
" self.surtype, self.surpara, self.trntype, self.unk7 = \\\n",
|
||
|
" surtype, surpara, trntype, unk7\n",
|
||
|
" self.vidxs = vidxs\n",
|
||
|
" self.verts = verts\n",
|
||
|
" self.minY = verts[:,1].min()\n",
|
||
|
" self.maxY = verts[:,1].max()\n",
|
||
|
" self.n = normalize(np.cross(verts[1]-verts[0], verts[2]-verts[1])) if n is None else n\n",
|
||
|
" self.c = -np.dot(verts[0], self.n) if c is None else c\n",
|
||
|
" def __repr__(self):\n",
|
||
|
" return 'minY=%.0f maxY=%.0f n=(%5.2f, %5.2f, %5.2f)'%(\n",
|
||
|
" self.minY, self.maxY, *self.n,\n",
|
||
|
" )\n",
|
||
|
" \n",
|
||
|
"def checkList2list(ptr):\n",
|
||
|
" ans = []\n",
|
||
|
" while True:\n",
|
||
|
" ptr, data = read_struct(ptr+4, '>II')\n",
|
||
|
" ans.append(Surface(\n",
|
||
|
" *read_struct(data, '>HHBB'),\n",
|
||
|
" np.array(read_struct(data+0x10, '>9f'), 'f').reshape(3, 3),\n",
|
||
|
" n=np.array(read_struct(data+0x34, '>3f'), 'f'),\n",
|
||
|
" c=d.read_float(data+0x40),\n",
|
||
|
" ))\n",
|
||
|
" if ptr == 0: return ans"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 4,
|
||
|
"id": "2c901433-7ee7-4771-9e89-1adc634ea889",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"makeTriPrism = lambda tri0, tri1: Polyhedron([\n",
|
||
|
" *tri0,\n",
|
||
|
" *tri1,\n",
|
||
|
"], [e for i in range(3) for e in [(i, (i+1)%3), (i, i+3), (i+3, (i+1)%3+3)]])\n",
|
||
|
"\n",
|
||
|
"def makeGround(tri, hG=0):\n",
|
||
|
" poly = makeTriPrism(tri.verts+(0,hG,0), tri.verts-(0,108,0))\n",
|
||
|
" poly.clipPlane(p=(0, tri.minY, 0), n=(0, 1, 0))\n",
|
||
|
" return poly\n",
|
||
|
"def makeRoof(tri, hR=82):\n",
|
||
|
" poly = makeTriPrism(tri.verts-(0,hR,0), tri.verts-(0,160,0))\n",
|
||
|
" return poly\n",
|
||
|
"def makeWall(tri, rW=50, dy=30):\n",
|
||
|
" verts = tri.verts - (0, dy, 0)\n",
|
||
|
" n = tri.n\n",
|
||
|
" off = (2*rW,0,0) if abs(n[0])>0.707 else (0,0,2*rW)\n",
|
||
|
" poly = makeTriPrism(verts-off, verts+off)\n",
|
||
|
" poly.clipPlane(p=verts[0], n=n, c=-rW)\n",
|
||
|
" poly.clipPlane(p=verts[0], n=-n, c=-rW)\n",
|
||
|
" return poly\n",
|
||
|
"\n",
|
||
|
"# make collision: Polyhedron[]\n",
|
||
|
"def makeCol(tri, airborne=True):\n",
|
||
|
" ny = tri.n[1]\n",
|
||
|
" if ny > 0.2:\n",
|
||
|
" return [makeGround(tri, 0 if airborne else 100)]\n",
|
||
|
" elif ny < -0.2:\n",
|
||
|
" return [makeRoof(tri, 82 if airborne else 2)]\n",
|
||
|
" else:\n",
|
||
|
" return [\n",
|
||
|
" makeWall(tri, 50, 150),\n",
|
||
|
" makeWall(tri, 50, 30),\n",
|
||
|
" ] if airborne else [\n",
|
||
|
" makeWall(tri, 25, 30),\n",
|
||
|
" makeWall(tri, 50, 60),\n",
|
||
|
" ]"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "17de8a60-e9eb-4507-9aaa-10e2deeba749",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"## Get data from Dolphin"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 5,
|
||
|
"id": "e2b4da62-691e-44ec-bb03-4893d644ec6b",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"# mario\n",
|
||
|
"gpMarioOriginal = 0x8040A378\n",
|
||
|
"ptrMario = d.read_uint32(gpMarioOriginal)\n",
|
||
|
"x, y, z = read_struct(ptrMario+0x10, '>3f')\n",
|
||
|
"\n",
|
||
|
"# get collision data (static collision)\n",
|
||
|
"gpMap = 0x8040A570\n",
|
||
|
"ptrMap = d.read_uint32(gpMap)\n",
|
||
|
"ptrCol = d.read_uint32(ptrMap+0x10)\n",
|
||
|
"xLimit, zLimit, xBlockCount, ptrStCLR = read_struct(ptrCol, '>ffI4x4xI')\n",
|
||
|
"## TBGCheckListRoot[zBlockCount][xBlockCount]\n",
|
||
|
"colOff = int((z+zLimit)//1024*xBlockCount + (x+xLimit)//1024)*36\n",
|
||
|
"## root->ground(12*2).next(4)\n",
|
||
|
"stWalls = checkList2list(d.read_uint32(ptrStCLR+colOff+4+12*2))"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "cbb5f4b3-c5c8-4b95-ba11-5de3b8e05f1c",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"## Draw wall hitbox"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 6,
|
||
|
"id": "5a6ef0d3-76e0-427d-9fb0-c3e5360c9199",
|
||
|
"metadata": {},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"image/png": "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
|
||
|
"text/plain": [
|
||
|
"<Figure size 576x576 with 1 Axes>"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {
|
||
|
"needs_background": "light"
|
||
|
},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"yy, airborne = 183, False\n",
|
||
|
"#yy, airborne = y, True\n",
|
||
|
"\n",
|
||
|
"# paras\n",
|
||
|
"margin = 0.05\n",
|
||
|
"\n",
|
||
|
"# vars\n",
|
||
|
"fig, ax = plt.subplots(figsize=(8,8))\n",
|
||
|
"xyMax = np.full(2, -np.inf)\n",
|
||
|
"xyMin = np.full(2, np.inf)\n",
|
||
|
"\n",
|
||
|
"# draw 2 hitboxs (draw in reverse order)\n",
|
||
|
"for ii in [1, 0]:\n",
|
||
|
" polys = [\n",
|
||
|
" (c, wall.n)\n",
|
||
|
" for wall in stWalls\n",
|
||
|
" for c in [makeCol(wall, airborne=airborne)[ii]]\n",
|
||
|
" # FIXME: filtering walls\n",
|
||
|
" if c.verts[:,2].min()<14050\n",
|
||
|
" ]\n",
|
||
|
" # arrow size scale (0.5 if radius is 25)\n",
|
||
|
" amul = 1.0 if airborne or ii == 1 else 0.5\n",
|
||
|
" # draw wall hitboxs (draw in reverse order)\n",
|
||
|
" for poly, n in polys[::-1]:\n",
|
||
|
" # clip at y=yy and take (x, z) coordinate\n",
|
||
|
" verts = poly.slicePlane((0, yy, 0), (0, 1, 0))[:,[0,2]]\n",
|
||
|
" p2 = Polygon(verts)\n",
|
||
|
" path = p2.path # path to plot\n",
|
||
|
" if path is None: continue # skip if no intersection\n",
|
||
|
" # plot hitbox area\n",
|
||
|
" patch = patches.PathPatch(path, facecolor='#88ff88e8', lw=1)\n",
|
||
|
" ax.add_patch(patch)\n",
|
||
|
" ## update x, y range\n",
|
||
|
" xyMax = array([xyMax, *verts]).max(axis=0)\n",
|
||
|
" xyMin = array([xyMin, *verts]).min(axis=0)\n",
|
||
|
" # plot arrow (TODO: can be improved)\n",
|
||
|
" ## center of the clipped polygon\n",
|
||
|
" vc = np.mean(verts, axis=0)\n",
|
||
|
" ## normal of the wall\n",
|
||
|
" n2 = normalize(n[[0,2]])\n",
|
||
|
" ## direction vector (perpendicular to normal vector)\n",
|
||
|
" l2 = np.array([-n2[1], n2[0]])\n",
|
||
|
" loff = np.matmul(verts-vc, [n2[1], -n2[0]])\n",
|
||
|
" loffp = loff[loff>0].min() if len(loff[loff>0]) else 0\n",
|
||
|
" loffm = loff[loff<0].max() if len(loff[loff<0]) else 0\n",
|
||
|
" loffdis = loffp-loffm\n",
|
||
|
" arsize0 = 30 # arrow size (base)\n",
|
||
|
" arsize = (arsize0 if loffdis<=arsize0*5 else loffdis/5)*amul\n",
|
||
|
" if loffdis <= arsize0: continue # if too small\n",
|
||
|
" ## offset parallel to direction vector\n",
|
||
|
" for loff in np.linspace(loffm+arsize*0.64, loffp-arsize*0.64, int(loffdis//arsize)) if loffdis>=arsize*2 else [0]:\n",
|
||
|
" ## offset parallel to normal vector\n",
|
||
|
" for off, l in [(-40, 37.5), (5.0, 37.5)]:\n",
|
||
|
" plt.arrow(\n",
|
||
|
" *vc+loff*l2+n2*off*amul, *n2*l*amul,\n",
|
||
|
" width=arsize*0.27, head_width=arsize*0.72, head_length=15*amul,\n",
|
||
|
" length_includes_head=True, color='#080'\n",
|
||
|
" )\n",
|
||
|
"\n",
|
||
|
"# set x, y range of the figure\n",
|
||
|
"xMg, yMg = (xyMax-xyMin)*margin\n",
|
||
|
"xMax, yMax = xyMax\n",
|
||
|
"xMin, yMin = xyMin\n",
|
||
|
"ax.set_xlim(xMin-xMg, xMax+xMg)\n",
|
||
|
"ax.set_ylim(yMin-yMg, yMax+yMg)\n",
|
||
|
"\n",
|
||
|
"# if needed\n",
|
||
|
"ax.invert_xaxis()\n",
|
||
|
"#ax.invert_yaxis()\n",
|
||
|
"ax.grid()\n",
|
||
|
"\n",
|
||
|
"plt.title(f'y = %s (%s)'%(yy, 'airborne' if airborne else 'grounded'))\n",
|
||
|
"#fig.patch.set_facecolor('white')\n",
|
||
|
"\n",
|
||
|
"plt.show()"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "6de1a895-e624-4fe4-a6e6-bc8a820ca88f",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": []
|
||
|
}
|
||
|
],
|
||
|
"metadata": {
|
||
|
"kernelspec": {
|
||
|
"display_name": "Python 3 (ipykernel)",
|
||
|
"language": "python",
|
||
|
"name": "python3"
|
||
|
},
|
||
|
"language_info": {
|
||
|
"codemirror_mode": {
|
||
|
"name": "ipython",
|
||
|
"version": 3
|
||
|
},
|
||
|
"file_extension": ".py",
|
||
|
"mimetype": "text/x-python",
|
||
|
"name": "python",
|
||
|
"nbconvert_exporter": "python",
|
||
|
"pygments_lexer": "ipython3",
|
||
|
"version": "3.9.1"
|
||
|
}
|
||
|
},
|
||
|
"nbformat": 4,
|
||
|
"nbformat_minor": 5
|
||
|
}
|