From 56d0ac20641a91d341279f5d19eda3743cf29d21 Mon Sep 17 00:00:00 2001 From: sup39 Date: Thu, 18 Nov 2021 19:44:40 +0900 Subject: [PATCH] init --- LICENSE | 22 ++ README.md | 18 ++ main.ipynb | 590 +++++++++++++++++++++++++++++++++++++++++++++++ memorylib.py | 253 ++++++++++++++++++++ requirements.txt | 3 + 5 files changed, 886 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 main.ipynb create mode 100644 memorylib.py create mode 100644 requirements.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3000b11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2021 sup39[サポミク] + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..53e8563 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# SMS-CM-gap +Brute forcing lava surface gap in Corona Mountain with Dolphin and Jupyter notebook. + +## Installation +Install Python 3.9 or higher, and use `pip` to install all packages in `requirements.txt`: +``` +pip install -r requirements.txt +``` + +## Usage +Start Jupyter notebook/lab and browse `main.ipynb`: +``` +jupyter notebook +``` + +## Credit +The file `memorylib.py` in this repo is a copy from +[QbeRoot/sms-livecol](https://github.com/QbeRoot/sms-livecol). \ No newline at end of file diff --git a/main.ipynb b/main.ipynb new file mode 100644 index 0000000..656b748 --- /dev/null +++ b/main.ipynb @@ -0,0 +1,590 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b20e1b96-585c-4cfe-899a-a12198b076f7", + "metadata": {}, + "source": [ + "# Brute forcing lava surface gap in Corona Mountain\n", + "[su(@ykpin64)'s demo 1](https://twitter.com/ykpin64/status/1439228134088331273) \n", + "[su(@ykpin64)'s demo 2](https://twitter.com/ykpin64/status/1439867660955623426) \n", + "[Coordinates within the gap near z=13260.290](https://docs.google.com/spreadsheets/d/117ut2qKKVrpSavebtknhrhqDuvPu3A9dRuK7i47dGSM/edit#gid=1250557014)\n", + "(by sup39)" + ] + }, + { + "cell_type": "markdown", + "id": "d5f2ea9b-5259-446e-9a1d-8fc31700fe67", + "metadata": {}, + "source": [ + "## Prerequisite\n", + "Make sure `memorylib.py` is in the same directory as this Jupyter notebook.\n", + "If you don't have one, [download it](https://raw.githubusercontent.com/QbeRoot/sms-livecol/main/memorylib.py) from\n", + "[QbeRoot/sms-livecol](https://github.com/QbeRoot/sms-livecol)\n", + "and put it in the same directory as this Jupyter notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09f159c9-77ad-41e0-b8ef-5ad35ab3afc0", + "metadata": {}, + "outputs": [], + "source": [ + "# Well, I don't know how to use wget in Windows\n", + "!wget https://raw.githubusercontent.com/QbeRoot/sms-livecol/main/memorylib.py" + ] + }, + { + "cell_type": "markdown", + "id": "8c595984-b034-4a0d-9e65-1ed443a0b562", + "metadata": { + "tags": [] + }, + "source": [ + "## Preparation" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d5037b8e-3916-4c33-8670-3c1c22e80b78", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from numpy import array, float32\n", + "import time\n", + "# float32 contants\n", + "inf = np.float32(np.inf)\n", + "minf = -inf\n", + "nan = np.float32(np.nan)\n", + "# Make sure memorylib.py is in the same directory\n", + "from memorylib import Dolphin" + ] + }, + { + "cell_type": "markdown", + "id": "20d8563d-ecdb-40af-95b8-5aee1b412db2", + "metadata": {}, + "source": [ + "### Initialize Dolphin\n", + "Open Dolphin and start Super Mario Sunshine, and then execute the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ac8f997d-e4e6-49e7-8b5f-7523eb1bce0d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1813322042992 0x1a6326a8e70\n" + ] + } + ], + "source": [ + "dolphin = Dolphin()\n", + "if not dolphin.find_dolphin():\n", + " print('Dolphin not found')\n", + "if not dolphin.init_shared_memory():\n", + " print('MEM1 not found')\n", + "if dolphin.read_ram(0, 3).tobytes() != b'GMS':\n", + " print('Current game is not Sunshine')" + ] + }, + { + "cell_type": "markdown", + "id": "846fc1f2-b921-4f58-8958-a48d5a8c3ae5", + "metadata": {}, + "source": [ + "If it says `MEM1 not found`, make sure your Dolphin is dev or beta version. **You can not use the stable 5.0 version**." + ] + }, + { + "cell_type": "markdown", + "id": "c6185fac-4925-46f9-8fd4-c4c1575d2c5f", + "metadata": {}, + "source": [ + "If no error occurs, proceed to enter Corona Mountain in Dolphin." + ] + }, + { + "cell_type": "markdown", + "id": "9dcb8308-bab5-41d4-9642-4224639d807f", + "metadata": {}, + "source": [ + "### Prevent Mario from dying when touching lava \n", + "The lava surface in Corona Mountain consists of two triangles. One is at memory `80EE82C8`, and the other is at `80EE8310`. For convenience, change the water(floor) type of these triangles to `0x104`, which Mario can stand on it without dying.\n", + "\n", + "Make sure to overwrite the water type **after** entering Corona Mountain." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5bd25bb7-c761-42fd-ae35-e837e2da5cbd", + "metadata": {}, + "outputs": [], + "source": [ + "dolphin.write_uint16(0x80EE82C8, 0x104)\n", + "dolphin.write_uint16(0x80EE8310, 0x104)" + ] + }, + { + "cell_type": "markdown", + "id": "d88e28b8-ee15-43cd-a1a3-12ac4402b166", + "metadata": {}, + "source": [ + "You can save state after setting the floor type,\n", + "and next time you just need to reload the state you saved\n", + "without setting the floor type again." + ] + }, + { + "cell_type": "markdown", + "id": "7455bb2d-ea35-4d5d-8ca4-cea10a36cb02", + "metadata": {}, + "source": [ + "### Prepare Memory Address\n", + "We need to set Mario's position, so we need to know the address of his X, Y, Z coordinate.\n", + "Also, in order to detect if the water surface is under Mario, we can check if \n", + "`TMario+0xEC`(Height of the floor below Mario) is 0(water surface) or -500(no water surface).\n", + "#### References\n", + "[Version magic number](https://github.com/QbeRoot/sms-livecol/blob/main/collision.py#L292) \n", + "[Absolute address of *gpMarioOriginal](https://docs.google.com/spreadsheets/d/1ElTW-akaTUF9OC2pIFR9-7aVPwpJ54AdEVJyJ_jvg0E/edit#gid=1727422135) \n", + "[RAM Map of TMario](https://docs.google.com/spreadsheets/d/1ElTW-akaTUF9OC2pIFR9-7aVPwpJ54AdEVJyJ_jvg0E/edit#gid=1550544746)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0d3832ab-4ba9-4786-bea3-8941379bb677", + "metadata": {}, + "outputs": [], + "source": [ + "# TMario**\n", + "ptrPtrMario = {\n", + " 0x23: 0x8040A378, # JP 1.0\n", + " 0xA3: 0x8040E0E8, # NA / KOR\n", + " 0x41: 0x804057B0, # PAL\n", + " 0x80: 0x8040A378, # JP 1.1 (Not sure)\n", + " # 0x4D: ????????, # 3DAS\n", + "}.get(dolphin.read_uint8(0x80365DDD))\n", + "\n", + "# TMario*\n", + "ptrMario = dolphin.read_uint32(ptrPtrMario)\n", + "ptrX, ptrY, ptrZ = (ptrMario+i for i in range(0x10, 0x18+1, 4))\n", + "ptrFloorHeight = ptrMario+0xec" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e2eb5f72-250d-460b-9c80-e7e9f673a549", + "metadata": {}, + "outputs": [], + "source": [ + "def write_position(x, y, z):\n", + " dolphin.write_float(ptrX, x)\n", + " dolphin.write_float(ptrY, y)\n", + " dolphin.write_float(ptrZ, z)" + ] + }, + { + "cell_type": "markdown", + "id": "0695afa9-3105-4026-8aa0-c02fa1d2e022", + "metadata": {}, + "source": [ + "Now, you can use `write_position(x, y, z)` to move Mario to any point you like.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "90a0ba5e-d858-4b27-891b-35129c9866c7", + "metadata": {}, + "outputs": [], + "source": [ + "write_position(float32(1302.07495), 100, float32(5962.14697))" + ] + }, + { + "cell_type": "markdown", + "id": "9fa16dca-6de4-4fba-a362-3e98b8c7ed75", + "metadata": {}, + "source": [ + "### Other utility functions\n", + "You may want to find the coordinate one by one,\n", + "and need to know the nearest previous/next float32.\n", + "To do that, you can simply use `numpy.nextafter(x, toward)`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dd09e269-3ad9-4677-b7a0-d571763c29f4", + "metadata": {}, + "outputs": [], + "source": [ + "nextf = lambda x: np.nextafter(float32(x), inf)\n", + "prevf = lambda x: np.nextafter(float32(x), minf)" + ] + }, + { + "cell_type": "markdown", + "id": "24a574ee-8988-4246-9abd-f307aa75707b", + "metadata": {}, + "source": [ + "For instance, the next float32 after `1` should be `1+2^-23`,\n", + "and the previous float32 before `1` should be `1-2^-24`.\n", + "\n", + "Note that you would better cast\n", + "python's `float`(which is handled as `float64`) into `numpy.float32` explicitly\n", + "to prevent unexpected type casting." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "72c7d34b-b473-4f8d-ac16-0afc2ddb9853", + "metadata": {}, + "outputs": [], + "source": [ + "assert nextf(1)-float32(1) == 2**-23\n", + "assert float32(1)-prevf(1) == 2**-24" + ] + }, + { + "cell_type": "markdown", + "id": "e4c51d0c-f894-469f-87c2-e99283687a84", + "metadata": {}, + "source": [ + "## Find coordinates within the gap\n", + "As mentioned above, we can test if a coordinate is within the gap by\n", + "moving Mario to the coordinate and check the floor height under Mario." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b3c7e4eb-92c6-42ee-969b-2dc29e4c3c5a", + "metadata": {}, + "outputs": [], + "source": [ + "# sleep 1/29.97 second, approximately 1 frame\n", + "# you can increase this variable if sometimes the return value is wrong\n", + "dt_sleep = 1/29.97\n", + "def test_xz(x, z):\n", + " write_position(x, 0, z)\n", + " time.sleep(dt_sleep)\n", + " return dolphin.read_float(ptrFloorHeight)<0 # True if no surface under Mario" + ] + }, + { + "cell_type": "markdown", + "id": "47661431-a351-4168-a2e3-d4dbbb541e80", + "metadata": {}, + "source": [ + "Now you can test any coordinate with `test_xz(x, z)`.\n", + "Note that float32 has low precision.\n", + "For example, for numbers between 4096 and 8192,\n", + "the smallest distance between two 2 floats is $4096\\times2^{-23}\\approx4.88\\times10^{-4}$.\n", + "i.e. only 4 digits after decimal point is meaningful." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9088b2c7-0a14-49d9-a6f1-370da9808c0c", + "metadata": {}, + "outputs": [], + "source": [ + "## ref. https://twitter.com/sup39x1207/status/1460915545595736072\n", + "assert test_xz(float32(1302.07495), float32(5962.14648))\n", + "assert test_xz(float32(1302.07495), float32(5962.14697))\n", + "assert not test_xz(float32(1302.07495), prevf(float32(5962.14648)))\n", + "assert not test_xz(float32(1302.07495), nextf(float32(5962.14697)))" + ] + }, + { + "cell_type": "markdown", + "id": "17c0c714-52fe-4f8e-841a-9b70d396f102", + "metadata": {}, + "source": [ + "To find the coordinates within the gap efficiently,\n", + "we can first calculate the theoretical boundary,\n", + "and then find coordinates near the boundary." + ] + }, + { + "cell_type": "markdown", + "id": "e65b874f-676d-4907-becc-35ed028d35fa", + "metadata": {}, + "source": [ + "For your information, the boundary between two lava surface triangles\n", + "(`80EE82C8` and `80EE8310`) is a segment from\n", + "`(-6000, 0, -33900)` to `(6200, 0, 32700)`\n", + "([Reference Image](https://twitter.com/ykpin64/status/1439233002677047299)).\n", + "\n", + "That is, given $x$, the z coordinate of the boundary should be\n", + "$z=-33900+\\frac{x+6000}{6200+6000}$. \n", + "Also, given $z$, the x coordinate should be $x=-6000+\\frac{z+33900}{32700+33900}$." + ] + }, + { + "cell_type": "markdown", + "id": "e0a5d07f-0472-490b-87c7-2fe329967c63", + "metadata": {}, + "source": [ + "Therefore, given z, we can use the following function to find the x range within the gap:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b01878bc-74f4-4a40-bbf5-aa3d67664888", + "metadata": {}, + "outputs": [], + "source": [ + "def find_x(z):\n", + " # theoretical x\n", + " x = float32((z+33900)/(32700+33900)*(6200+6000)-6000)\n", + " # prev/next x to test\n", + " xp1, xn1 = (prevf(x), nextf(x))\n", + " # the actual x_min/x_max within the gap\n", + " xp, xn = (x, x) if test_xz(x, z) else (xn1, xp1)\n", + " # find x_min. test at least 2 floats\n", + " if test_xz(xp1, z): xp = xp1\n", + " xp1 = prevf(xp1)\n", + " while test_xz(xp1, z): xp, xp1 = xp1, prevf(xp1)\n", + " # find x_max. test at least 2 floats\n", + " if test_xz(xn1, z): xn = xn1\n", + " xn1 = nextf(xn1)\n", + " while test_xz(xn1, z): xn, xn1 = xn1, nextf(xn1)\n", + " # return z and x range. if xp<=xn, it means no x is valid\n", + " return (z, xp, xn) if xp <= xn else (z, None, None)" + ] + }, + { + "cell_type": "markdown", + "id": "686b5d7f-c7f2-4684-bd58-3f65857f86fd", + "metadata": {}, + "source": [ + "Note that if the actual gap is too far away from its theoretical value,\n", + "this function may cause a false negative\n", + "(and that's why I test at least 2 floats in each direction).\n", + "You can test more floats near the theoretical value if you want." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "904e2e7e-a255-46c5-a6b1-2cbfded90e6f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(5962.1, 1302.0662, 1302.0667)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "find_x(float32(5962.1))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "eb4c216d-6f48-4c02-bd03-607301c49f33", + "metadata": {}, + "outputs": [], + "source": [ + "assert not test_xz(prevf(float32(1302.0662)), float32(5962.1))\n", + "assert test_xz(float32(1302.0662), float32(5962.1))\n", + "assert test_xz(float32(1302.0664), float32(5962.1))\n", + "assert test_xz(float32(1302.0667), float32(5962.1))\n", + "assert not test_xz(nextf(float32(1302.0667)), float32(5962.1))" + ] + }, + { + "cell_type": "markdown", + "id": "2ac1a4f9-1722-4ae7-85a0-301ea52822df", + "metadata": {}, + "source": [ + "Finally, you may want to make a loop of z to find more points automatically.\n", + "I recommend use `tqdm` to track progress and estimate time." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1986e131-60fa-4176-bc74-b26f3f6094c5", + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm.notebook import tqdm" + ] + }, + { + "cell_type": "markdown", + "id": "4ef6eab2-1740-4a1e-8134-d33c4265c0ff", + "metadata": {}, + "source": [ + "There are many ways to do a loop. The following code is the one sup39 used." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b65d4ccf-9ef3-48ff-b7e4-5e59d77f21be", + "metadata": {}, + "outputs": [], + "source": [ + "z0 = np.float32(13260.29) # Initial z value\n", + "zp = zn = z0\n", + "result = [find_x(z0)]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4ac3ab10-7c1f-4e0b-b470-7fa75594e980", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bdfc6d7b562d43619394c49b1370bb17", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/50 [00:00= 0x80000000 + value = self.read_ram(addr-0x80000000, 1) + + return unpack(">B", value)[0] + + def write_uint8(self, addr, val): + assert addr >= 0x80000000 + return self.write_ram(addr - 0x80000000, pack(">B", val)) + + def read_uint16(self, addr): + assert addr >= 0x80000000 + value = self.read_ram(addr-0x80000000, 2) + + return unpack(">H", value)[0] + + def write_uint16(self, addr, val): + assert addr >= 0x80000000 + return self.write_ram(addr - 0x80000000, pack(">H", val)) + + def read_uint32(self, addr): + assert addr >= 0x80000000 + value = self.read_ram(addr-0x80000000, 4) + + return unpack(">I", value)[0] + + def write_uint32(self, addr, val): + assert addr >= 0x80000000 + return self.write_ram(addr - 0x80000000, pack(">I", val)) + + def read_float(self, addr): + assert addr >= 0x80000000 + value = self.read_ram(addr - 0x80000000, 4) + + return unpack(">f", value)[0] + + def write_float(self, addr, val): + assert addr >= 0x80000000 + return self.write_ram(addr - 0x80000000, pack(">f", val)) + + +"""with open("ctypes.txt", "w") as f: + for a in ctypes.__dict__: + f.write(str(a)) + f.write("\n")""" + +if __name__ == "__main__": + dolphin = Dolphin() + import multiprocessing + + if dolphin.find_dolphin(): + + print("Found Dolphin!") + else: + print("Didn't find Dolphin") + + print(dolphin.pid) + + dolphin.init_shared_memory() + if dolphin.init_shared_memory(): + print("We found MEM1 and/or MEM2!") + else: + print("We didn't find it...") + + import random + randint = random.randint + from timeit import default_timer + + start = default_timer() + + print("Testing Shared Memory Method") + start = default_timer() + count = 500000 + for i in range(count): + value = randint(0, 2**32-1) + dolphin.write_uint32(0x80000000, value) + + result = dolphin.read_uint32(0x80000000) + assert result == value + diff = default_timer()-start + print(count/diff, "per sec") + print("time: ", diff) + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5672e92 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy +tqdm +jupyter \ No newline at end of file