From 83f096b1db397662f447ac64d106a956bd07bf03 Mon Sep 17 00:00:00 2001 From: QbeRoot Date: Sat, 9 Oct 2021 05:59:27 +0200 Subject: [PATCH] collision viewer --- .gitignore | 1 + README.md | 4 +- collision.py | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++ memorylib.py | 20 ++++ 4 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 collision.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md index e2cfd74..4d0bb35 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# dolphin-memory-lib -A Python library for reading and writing the memory of an emulated game in Dolphin. +# sms-livecol +A live collision viewer for Super Mario Sunshine running on Dolphin. \ No newline at end of file diff --git a/collision.py b/collision.py new file mode 100644 index 0000000..ca8a0bc --- /dev/null +++ b/collision.py @@ -0,0 +1,260 @@ +import sys, pyrr + +from PyQt5 import QtCore +from PyQt5 import QtGui +from PyQt5 import QtWidgets + +from OpenGL.GL import * +from OpenGL.GLUT import * +from OpenGL.GLU import * +from OpenGL.arrays import vbo +from OpenGL.GL import shaders + +from numpy import array + +from memorylib import Dolphin + +class CollisionViewer(QtWidgets.QOpenGLWidget): + gpCamera = 0 + gpMapCollisionData = 0 + + def __init__(self, dolphin: Dolphin, parent=None): + self.dolphin = dolphin + self.parent = parent + QtWidgets.QOpenGLWidget.__init__(self, parent) + self.resize(800, 600) + self.frameSwapped.connect(self.update) + + def initializeGL(self) -> None: + glEnable(GL_DEPTH_TEST) + glEnable(GL_CULL_FACE) + glClearColor(0.7, 0.7, 1.0, 0.0) + + self.shader = shaders.compileProgram( + shaders.compileShader("""#version 330 core + uniform mat4 projMat; + uniform mat4 viewMat; + + layout (location = 0) in vec3 position; + layout (location = 1) in float type; + + out vec4 vVertexColor; + + void main() { + gl_Position = projMat * viewMat * vec4(position, 1.0); + + if (type == 0) { + vVertexColor = vec4(0, 0, 1, 1); + } else if (type == 1) { + vVertexColor = vec4(1, 0, 0, 1); + } else if (type == 2) { + vVertexColor = vec4(0, 1, 0, 1); + } else if (type == 3) { + vVertexColor = vec4(0, 0.5, 0, 1); + } else { + vVertexColor = vec4(0.5, 0.5, 0.5, 1); + } + }""", GL_VERTEX_SHADER), + shaders.compileShader("""#version 330 core + layout(triangles) in; + layout(triangle_strip, max_vertices = 3) out; + + in vec4 vVertexColor[3]; + out vec3 gTriDistance; + out float gTriSize; + out vec4 gVertexColor; + + void main() { + gTriSize = max(max(distance(gl_in[0].gl_Position, gl_in[1].gl_Position), + distance(gl_in[0].gl_Position, gl_in[2].gl_Position)), + distance(gl_in[1].gl_Position, gl_in[2].gl_Position)); + + gTriDistance = vec3(1, 0, 0); + gVertexColor = vVertexColor[0]; + gl_Position = gl_in[0].gl_Position; + EmitVertex(); + + gTriDistance = vec3(0, 1, 0); + gVertexColor = vVertexColor[1]; + gl_Position = gl_in[1].gl_Position; + EmitVertex(); + + gTriDistance = vec3(0, 0, 1); + gVertexColor = vVertexColor[2]; + gl_Position = gl_in[2].gl_Position; + EmitVertex(); + + EndPrimitive(); + }""", GL_GEOMETRY_SHADER), + shaders.compileShader("""#version 330 core + in vec3 gTriDistance; + in float gTriSize; + in vec4 gVertexColor; + out vec4 color; + + float amplify(float d, float scale, float offset) { + d = scale * d + offset; + d = clamp(d, 0, 1); + d = 1 - exp2(-2*d*d); + return d; + } + + void main() { + float d1 = min(min(gTriDistance.x, gTriDistance.y), gTriDistance.z); + color = smoothstep(0, fwidth(d1), d1) * gVertexColor; + }""", GL_FRAGMENT_SHADER)) + + self.vao = glGenVertexArrays(1) + + def getCheckData(self, checkList): + out = set() + + while checkList >= 0x80000000: + checkData = self.dolphin.read_uint32(checkList + 0x8) + if checkData >= 0x80000000: + out.add(checkData) + checkList = self.dolphin.read_uint32(checkList + 0x4) + + return out + + + def paintGL(self) -> None: + if self.gpCamera == 0 or self.gpMapCollisionData == 0: + return + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + + camera = self.dolphin.read_uint32(self.gpCamera) + if camera == 0: + return + + projMat = pyrr.matrix44.create_perspective_projection_matrix(self.dolphin.read_float(camera + 0x48), self.aspect, + self.dolphin.read_float(camera + 0x28), self.dolphin.read_float(camera + 0x2C)) + viewMat = pyrr.matrix44.create_look_at( + [self.dolphin.read_float(camera + 0x124), self.dolphin.read_float(camera + 0x128), self.dolphin.read_float(camera + 0x12C)], + [self.dolphin.read_float(camera + 0x148), self.dolphin.read_float(camera + 0x14C), self.dolphin.read_float(camera + 0x150)], + [self.dolphin.read_float(camera + 0x30), self.dolphin.read_float(camera + 0x34), self.dolphin.read_float(camera + 0x38)]) + + mapColData = self.dolphin.read_uint32(self.gpMapCollisionData) + if mapColData == 0: + return + + checkListCount = self.dolphin.read_uint32(mapColData + 0x10) + checkLists1 = self.dolphin.read_uint32(mapColData + 0x14) + checkLists2 = self.dolphin.read_uint32(mapColData + 0x18) + + floors = set() + roofs = set() + walls = set() + + for i in range(checkListCount): + if checkLists1 != 0: + floors |= self.getCheckData(self.dolphin.read_uint32(checkLists1 + 0x24 * i + 0x4)) + roofs |= self.getCheckData(self.dolphin.read_uint32(checkLists1 + 0x24 * i + 0x10)) + walls |= self.getCheckData(self.dolphin.read_uint32(checkLists1 + 0x24 * i + 0x1C)) + + if checkLists2 != 0: + floors |= self.getCheckData(self.dolphin.read_uint32(checkLists2 + 0x24 * i + 0x4)) + roofs |= self.getCheckData(self.dolphin.read_uint32(checkLists2 + 0x24 * i + 0x10)) + walls |= self.getCheckData(self.dolphin.read_uint32(checkLists2 + 0x24 * i + 0x1C)) + + buffer = [] + + for f in floors: + buffer += [ + [self.dolphin.read_float(f + 0x10), self.dolphin.read_float(f + 0x14), self.dolphin.read_float(f + 0x18), 0], + [self.dolphin.read_float(f + 0x1C), self.dolphin.read_float(f + 0x20), self.dolphin.read_float(f + 0x24), 0], + [self.dolphin.read_float(f + 0x28), self.dolphin.read_float(f + 0x2C), self.dolphin.read_float(f + 0x30), 0] + ] + + for r in roofs: + buffer += [ + [self.dolphin.read_float(r + 0x10), self.dolphin.read_float(r + 0x14), self.dolphin.read_float(r + 0x18), 1], + [self.dolphin.read_float(r + 0x1C), self.dolphin.read_float(r + 0x20), self.dolphin.read_float(r + 0x24), 1], + [self.dolphin.read_float(r + 0x28), self.dolphin.read_float(r + 0x2C), self.dolphin.read_float(r + 0x30), 1] + ] + + for w in walls: + proj = 3 if self.dolphin.read_uint16(w + 0x4) & 0x8 else 2 + buffer += [ + [self.dolphin.read_float(w + 0x10), self.dolphin.read_float(w + 0x14), self.dolphin.read_float(w + 0x18), proj], + [self.dolphin.read_float(w + 0x1C), self.dolphin.read_float(w + 0x20), self.dolphin.read_float(w + 0x24), proj], + [self.dolphin.read_float(w + 0x28), self.dolphin.read_float(w + 0x2C), self.dolphin.read_float(w + 0x30), proj] + ] + + glUseProgram(self.shader) + + glUniformMatrix4fv(glGetUniformLocation(self.shader, 'projMat'), 1, False, projMat) + glUniformMatrix4fv(glGetUniformLocation(self.shader, 'viewMat'), 1, False, viewMat) + + glBindVertexArray(self.vao) + vertexBuffer = vbo.VBO(array(buffer, 'f')) + try: + vertexBuffer.bind() + try: + glEnableVertexAttribArray(0) + glVertexAttribPointer(0, 3, GL_FLOAT, False, 16, vertexBuffer) + glEnableVertexAttribArray(1) + glVertexAttribPointer(1, 1, GL_FLOAT, False, 16, vertexBuffer + 12) + glDrawArrays(GL_TRIANGLES, 0, len(buffer)) + finally: + vertexBuffer.unbind() + finally: + glBindVertexArray(0) + glUseProgram(0) + + def resizeGL(self, w: int, h: int) -> None: + self.width = w + self.height = h or 1 + self.aspect = self.width / self.height + glViewport(0, 0, self.width, self.height) + +def connect(): + if not dolphin.find_dolphin(): + status.showMessage('Dolphin not found') + return + + if not dolphin.init_shared_memory(): + status.showMessage('MEM1 not found') + return + + if dolphin.read_ram(0, 3).tobytes() != b'GMS': + status.showMessage('Current game is not Sunshine') + return + + viewer.gpCamera, viewer.gpMapCollisionData = { + 0x23: (0x8040B370, 0x8040A578), # JP 1.0 + 0xA3: (0x8040D0A8, 0x8040DEA0), # NA / KOR + 0x41: (0x80404808, 0x80405568), # PAL + 0x80: (0x803FFA38, 0x803FED40), # JP 1.1 + 0x4D: (0x80401D08, 0x80402A68), # 3DAS + }.get(dolphin.read_uint8(0x80365DDD)) + + status.showMessage('Ready') + +if __name__ == '__main__': + dolphin = Dolphin() + + app = QtWidgets.QApplication(sys.argv) + + window = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(window) + viewer = CollisionViewer(dolphin) + button = QtWidgets.QPushButton('Connect to Dolphin') + status = QtWidgets.QStatusBar() + + button.clicked.connect(connect) + + layout.addWidget(viewer) + layout.addWidget(button) + layout.addWidget(status) + layout.setStretch(0, 1) + + window.setWindowTitle('Super Mario Sunshine Live Collision Viewer') + window.resize(800, 600) + window.show() + + try: + sys.exit(app.exec()) + except SystemExit: + pass \ No newline at end of file diff --git a/memorylib.py b/memorylib.py index 5b1875b..a0e67d0 100644 --- a/memorylib.py +++ b/memorylib.py @@ -163,6 +163,26 @@ class Dolphin(object): def write_ram(self, offset, data): self.memory.buf[offset:offset+len(data)] = data + def read_uint8(self, addr): + assert addr >= 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)