<jupyter_start><jupyter_text>Brute forcing lava surface gap in Corona Mountain[su(@ykpin64)'s demo 1](https://twitter.com/ykpin64/status/1439228134088331273)  [su(@ykpin64)'s demo 2](https://twitter.com/ykpin64/status/1439867660955623426)  [Coordinates within the gap near z=13260.290](https://docs.google.com/spreadsheets/d/117ut2qKKVrpSavebtknhrhqDuvPu3A9dRuK7i47dGSM/editgid=1250557014)(by sup39)  PrerequisiteMake sure `memorylib.py` is in the same directory as this Jupyter notebook.If you don't have one, [download it](https://raw.githubusercontent.com/QbeRoot/sms-livecol/main/memorylib.py) from[QbeRoot/sms-livecol](https://github.com/QbeRoot/sms-livecol)and put it in the same directory as this Jupyter notebook.<jupyter_code># Well, I don't know how to use wget in Windows
!wget https://raw.githubusercontent.com/QbeRoot/sms-livecol/main/memorylib.py<jupyter_output><empty_output><jupyter_text>Preparation<jupyter_code>import numpy as np
from numpy import array, float32
import time
# float32 contants
inf = np.float32(np.inf)
minf = -inf
nan = np.float32(np.nan)
# Make sure memorylib.py is in the same directory
from memorylib import Dolphin<jupyter_output><empty_output><jupyter_text>Initialize DolphinOpen Dolphin and start Super Mario Sunshine, and then execute the following code:<jupyter_code>dolphin = Dolphin()
if not dolphin.find_dolphin():
  print('Dolphin not found')
if not dolphin.init_shared_memory():
  print('MEM1 not found')
if dolphin.read_ram(0, 3).tobytes() != b'GMS':
  print('Current game is not Sunshine')<jupyter_output>1813322042992 0x1a6326a8e70<jupyter_text>If it says `MEM1 not found`, make sure your Dolphin is dev or beta version. **You can not use the stable 5.0 version**. If no error occurs, proceed to enter Corona Mountain in Dolphin.  Prevent Mario from dying when touching lava 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.Make sure to overwrite the water type **after** entering Corona Mountain.<jupyter_code>dolphin.write_uint16(0x80EE82C8, 0x104)
dolphin.write_uint16(0x80EE8310, 0x104)<jupyter_output><empty_output><jupyter_text>You can save state after setting the floor type,and next time you just need to reload the state you savedwithout setting the floor type again.  Prepare Memory AddressWe need to set Mario's position, so we need to know the address of his X, Y, Z coordinate.Also, in order to detect if the water surface is under Mario, we can check if `TMario+0xEC`(Height of the floor below Mario) is 0(water surface) or -500(no water surface). References[Version magic number](https://github.com/QbeRoot/sms-livecol/blob/main/collision.pyL292)  [Absolute address of *gpMarioOriginal](https://docs.google.com/spreadsheets/d/1ElTW-akaTUF9OC2pIFR9-7aVPwpJ54AdEVJyJ_jvg0E/editgid=1727422135)  [RAM Map of TMario](https://docs.google.com/spreadsheets/d/1ElTW-akaTUF9OC2pIFR9-7aVPwpJ54AdEVJyJ_jvg0E/editgid=1550544746)<jupyter_code># TMario**
ptrPtrMario = {
  0x23: 0x8040A378, # JP 1.0
  0xA3: 0x8040E0E8, # NA / KOR
  0x41: 0x804057B0, # PAL
  0x80: 0x8040A378, # JP 1.1 (Not sure)
  # 0x4D: ????????, # 3DAS
}.get(dolphin.read_uint8(0x80365DDD))

# TMario*
ptrMario = dolphin.read_uint32(ptrPtrMario)
ptrX, ptrY, ptrZ = (ptrMario+i for i in range(0x10, 0x18+1, 4))
ptrFloorHeight = ptrMario+0xec
def write_position(x, y, z):
  dolphin.write_float(ptrX, x)
  dolphin.write_float(ptrY, y)
  dolphin.write_float(ptrZ, z)<jupyter_output><empty_output><jupyter_text>Now, you can use `write_position(x, y, z)` to move Mario to any point you like.<jupyter_code>write_position(float32(1302.07495), 100, float32(5962.14697))<jupyter_output><empty_output><jupyter_text>Other utility functionsYou may want to find the coordinate one by one,and need to know the nearest previous/next float32.To do that, you can simply use `numpy.nextafter(x, toward)`.<jupyter_code>nextf = lambda x: np.nextafter(float32(x), inf)
prevf = lambda x: np.nextafter(float32(x), minf)<jupyter_output><empty_output><jupyter_text>For instance, the next float32 after `1` should be `1+2^-23`,and the previous float32 before `1` should be `1-2^-24`.Note that you would better castpython's `float`(which is handled as `float64`) into `numpy.float32` explicitlyto prevent unexpected type casting.<jupyter_code>assert nextf(1)-float32(1) == 2**-23
assert float32(1)-prevf(1) == 2**-24<jupyter_output><empty_output><jupyter_text>Find coordinates within the gapAs mentioned above, we can test if a coordinate is within the gap bymoving Mario to the coordinate and check the floor height under Mario.<jupyter_code># sleep 1/29.97 second, approximately 1 frame
# you can increase this variable if sometimes the return value is wrong
dt_sleep = 1/29.97
def test_xz(x, z):
  write_position(x, 0, z)
  time.sleep(dt_sleep)
  return dolphin.read_float(ptrFloorHeight)<0 # True if no surface under Mario<jupyter_output><empty_output><jupyter_text>Now you can test any coordinate with `test_xz(x, z)`.Note that float32 has low precision.For example, for numbers between 4096 and 8192,the smallest distance between two 2 floats is $4096\times2^{-23}\approx4.88\times10^{-4}$.i.e. only 4 digits after decimal point is meaningful.<jupyter_code>## ref. https://twitter.com/sup39x1207/status/1460915545595736072
assert test_xz(float32(1302.07495), float32(5962.14648))
assert test_xz(float32(1302.07495), float32(5962.14697))
assert not test_xz(float32(1302.07495), prevf(float32(5962.14648)))
assert not test_xz(float32(1302.07495), nextf(float32(5962.14697)))<jupyter_output><empty_output><jupyter_text>To find the coordinates within the gap efficiently,we can first calculate the theoretical boundary,and then find coordinates near the boundary. For your information, the boundary between two lava surface triangles(`80EE82C8` and `80EE8310`) is a segment from`(-6000, 0, -33900)` to `(6200, 0, 32700)`([Reference Image](https://twitter.com/ykpin64/status/1439233002677047299)).That is, given $x$, the z coordinate of the boundary should be$z=-33900+\frac{x+6000}{6200+6000}$.  Also, given $z$, the x coordinate should be $x=-6000+\frac{z+33900}{32700+33900}$. Therefore, given z, we can use the following function to find the x range within the gap:<jupyter_code>def find_x(z):
  # theoretical x
  x = float32((z+33900)/(32700+33900)*(6200+6000)-6000)
  # prev/next x to test
  xp1, xn1 = (prevf(x), nextf(x))
  # the actual x_min/x_max within the gap
  xp, xn = (x, x) if test_xz(x, z) else (xn1, xp1)
  # find x_min. test at least 2 floats
  if test_xz(xp1, z): xp = xp1
  xp1 = prevf(xp1)
  while test_xz(xp1, z): xp, xp1 = xp1, prevf(xp1)
  # find x_max. test at least 2 floats
  if test_xz(xn1, z): xn = xn1
  xn1 = nextf(xn1)
  while test_xz(xn1, z): xn, xn1 = xn1, nextf(xn1)
  # return z and x range. if xp<=xn, it means no x is valid
  return (z, xp, xn) if xp <= xn else (z, None, None)<jupyter_output><empty_output><jupyter_text>Note that if the actual gap is too far away from its theoretical value,this function may cause a false negative(and that's why I test at least 2 floats in each direction).You can test more floats near the theoretical value if you want.<jupyter_code>find_x(float32(5962.1))
assert not test_xz(prevf(float32(1302.0662)), float32(5962.1))
assert test_xz(float32(1302.0662), float32(5962.1))
assert test_xz(float32(1302.0664), float32(5962.1))
assert test_xz(float32(1302.0667), float32(5962.1))
assert not test_xz(nextf(float32(1302.0667)), float32(5962.1))<jupyter_output><empty_output><jupyter_text>Finally, you may want to make a loop of z to find more points automatically.I recommend use `tqdm` to track progress and estimate time.<jupyter_code>from tqdm.notebook import tqdm<jupyter_output><empty_output><jupyter_text>There are many ways to do a loop. The following code is the one sup39 used.<jupyter_code>z0 = np.float32(13260.29) # Initial z value
zp = zn = z0
result = [find_x(z0)]
loop_count = 50 # change to any number you like

# write the result to result.csv
with open('result.csv', 'w') as fw:
  # utility functions
  write_row = lambda r: print(
    *('' if x is None else x for x in r),
    '' if r[1] is None else r[2]-r[1]+(nextf(r[2])-r[2])/2+(r[1]-prevf(r[1]))/2,
    sep=',', file=fw,
  )
  def append_row(r):
    result.append(r)
    write_row(r)
  # header
  print('z', 'x Min', 'x Max', 'x Range', sep=',', file=fw)
  # write existing result
  for r in result: write_row(r)
  # loop z
  for _ in tqdm(range(loop_count)):
    zp, zn = prevf(zp), nextf(zn)
    append_row(find_x(zp))
    append_row(find_x(zn))<jupyter_output><empty_output><jupyter_text>LICENSE<jupyter_code>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.<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10753891545426798, 'fraction_numerical': 0.07538915454267983, 'mean_word_length': 5.606182795698925, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 0, '>': 60, 'https://': 11, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '209597', 'license': 'MIT', 'n_tokens_mistral': 3911, 'n_tokens_neox': 3425, 'n_words': 1078, 'path': 'main.ipynb', 'repo_name': 'sup39/SMS-CM-gap', 'size': '9829'}