Skip to content

Instantly share code, notes, and snippets.

@Alexrns
Created February 8, 2025 08:16
Show Gist options
  • Save Alexrns/5faf4030e9aca89e6bb331e0c8c22e88 to your computer and use it in GitHub Desktop.
Save Alexrns/5faf4030e9aca89e6bb331e0c8c22e88 to your computer and use it in GitHub Desktop.
'''
Name: sc_map.py
Version: b1.4
Author: Alexrns
Date: 2/6/2025
Source and Config: https://gist.github.com/Alexrns/5faf4030e9aca89e6bb331e0c8c22e88
Description: STILL IN DEVELOPMENT
Changelog:
b1.0 - First public release.
b1.1 - Added more config options and made a config section.
- Should reset in boss.
- Map shouldnt overlap now.
b1.2 - Added color functionality for main room, normal room, current room, and end room.
- Rooms that lead back to main are indicated.
- Should reset when entering sc boss now.
b1.3 - Will reset when entering sc boss now.
- Added temporary(?) recursion error handling in all recursive loops.
Graphic Y is dependant on next room door location when current room door is middle
b1.4 - Turned door codes into (int) enum.
- Added fixes for dist and graphic loops.
TODO:
Verify that dist_from_main update loop is stable.
Draw connection grapics.
Verify that room graphics are accurate.
Offset left / right rooms based on `next_code`.
Reconsider storage method
Binary Search Tree esque approach
Reset plugin when homing out of incomplete sc.
Connection class is ugly, make edges a part of the dict?
'''
import util
import logging
import math
from _remote import ffi, lib
from manager import PluginBase
from enum import IntEnum
# door code key: left right top_left bottom_left top_right bottom_right
class DoorCodes(IntEnum):
DOOR_NONE = 0b000000
DOOR_LEFT = 0b100000
DOOR_RIGHT = 0b010000
DOOR_TOP_LEFT = 0b001000
DOOR_BOTTOM_LEFT = 0b000100
DOOR_TOP_RIGHT = 0b000010
DOOR_BOTTOM_RIGHT = 0b000001
NUM_DOOR = 6
class Connection:
def __init__(self, this_code=DoorCodes.DOOR_NONE, next_code=DoorCodes.DOOR_NONE, next_room=None):
self.this_code = this_code
self.next_code = next_code
self.next_room = next_room
# self.x1 = 0
# self.y1 = 0
# self.x2 = 0
# self.y2 = 0
def __eq__(self, other):
return self.this_code == other.this_code and self.next_code == other.next_code and self.next_room == other.next_room
def __repr__(self):
return f'{self.this_code:06b}-{self.next_code:06b}-{self.next_room.id}'
# return f'{self.next_room.id}'
def __str__(self):
return f'{self.this_code:06b}-{self.next_code:06b}-{self.next_room.id}'
# return f'{self.next_room.id}'
class Room:
def __init__(self, id=-1):
self.id = id
# door code key: left right top_left bottom_left top_right bottom_right
self.x = 0
self.y = 0
self.color = 0x80808080
self.dirty = False
self.door_codes = DoorCodes.DOOR_NONE
self.num_doors = 0
self.dist_from_main = -1
self.path_back = False
self.connections = []
# door_code: orderd pair / tuple - coords
self.door_locations = {
DoorCodes.DOOR_LEFT: (0, 0),
DoorCodes.DOOR_RIGHT: (0, 0),
DoorCodes.DOOR_TOP_LEFT: (0, 0),
DoorCodes.DOOR_BOTTOM_LEFT: (0, 0),
DoorCodes.DOOR_TOP_RIGHT: (0, 0),
DoorCodes.DOOR_BOTTOM_RIGHT: (0, 0),
}
def __eq__(self, other):
return self.id == other.id
# def __int__(self):
# return self.id
def __str__(self):
# return f'id: {self.id}, door_codes: {self.door_codes:06b}, ({self.x}, {self.y})({self.w}x{self.h}), connections: {self.connections[0] if self.connections else "[]"}, door_locations: {self.door_locations}'
# return f'id: {self.id}, door_codes: {self.door_codes:06b}, ({self.x}, {self.y}) {self.w}x{self.h}, connections: {self.connections}'
return f'id: {self.id}, dist: {self.dist_from_main}, path: {self.path_back}, door_codes: {self.door_codes:06b}, connections: {self.connections}'
def dist(x, y, d):
return math.floor(math.sqrt(pow(x - d[0], 2) + pow(y - d[1], 2)))
class Plugin(PluginBase):
def onInit(self, inputs=None):
self.config.option('show_map', False, 'bool')
self.config.option('show_debug', False, 'bool')
self.config.option('x', 350, 'int')
self.config.option('y', 250, 'int')
self.config.option('w', 50, 'int')
self.config.option('h', 50, 'int')
self.config.option('horizontal_spacing', 25, 'int')
self.config.option('vertical_spacing', 25, 'int')
self.config.option('current_room_color', 0x80ff8080, 'color')
self.config.option('main_room_color', 0x800080ff, 'color')
self.config.option('normal_room_color', 0x80ff80ff, 'color')
self.config.option('end_room_color', 0x8080ff00, 'color')
self.debug_txt = util.MultilineText()
# floor: Room
self.rooms = {}
# for the graphic
self.connections = []
self.main = None
self.first_enter = True
self.update_graphic = True
self.prev_floor = 0
self.floor = 0
self.prev_room = None
self.room = None
self.prev_closest_door = DoorCodes.DOOR_NONE
self.closest_door = DoorCodes.DOOR_NONE
# self.prev_decoration = b''
self.decoration = b''
self.d = 0
def initVals(self):
self.rooms.clear()
self.connections.clear()
self.main = None
self.first_enter = True
self.update_graphic = True
self.prev_floor = 0
self.floor = 0
self.prev_room = None
self.room = None
self.prev_closest_door = DoorCodes.DOOR_NONE
self.closest_door = DoorCodes.DOOR_NONE
self.decoration = b''
self.d = 0
def makeConnection(self):
test = 0
# Make no connections with core rooms
# if self.prev_decoration == b'sccore' or self.decoration == b'sccore': return
# logging.info('\n/ makeConnection -----------------------------------\\' +\
# f'\n{self.prev_room} - {self.prev_closest_door:06b}\n{self.room} - {self.closest_door:06b}')
# create connection
con = Connection(self.closest_door, self.prev_closest_door, self.prev_room)
logging.info(f'{con}')
if con not in self.room.connections:
test = 1
self.update_graphic = True
self.room.connections.append(con)
self.connections.append(con)
# create connection in other room
con = Connection(self.prev_closest_door, self.closest_door, self.room)
# logging.info(f'{con}' +\
# '\n\\---------------------------------------------------/')
if con not in self.prev_room.connections:
if not test:
logging.warn('room connection already existed but prev_room connection didnt!')
self.update_graphic = True
self.prev_room.connections.append(con)
def identifyDoors(self, room):
# FF / SC: facing 0 = right, facing 180 = left
# door code key: left, right, top_left, bottom_left, top_right, bottom_right
code = DoorCodes.DOOR_NONE
left_doors = 0
right_doors = 0
door_objs = []
for obj in util.worldobjects(self.refs.ClientWorld.serverSubWorld): # here I go looping again...
# look for closed doors
if ffi.string(obj.props.vid.s) in {b'sc.door', b'sc.cdoor'}:
# if ffi.string(obj.props.vid.s) in {b'forest.nextdoor', b'forest.prevdoor', b'forest.unextdoor', b'forest.uprevdoor'}:
door_objs.append(obj)
if obj.props.facing == 0:
right_doors += 1
elif obj.props.facing == 180:
left_doors += 1
if left_doors == 1:
code = code | DoorCodes.DOOR_LEFT
for obj in door_objs:
if obj.props.facing == 180:
room.door_locations[DoorCodes.DOOR_LEFT] = (obj.props.xmp, obj.props.ymp)
break
elif left_doors == 2:
code = code | DoorCodes.DOOR_TOP_LEFT | DoorCodes.DOOR_BOTTOM_LEFT
# find top / bot
pos = []
for obj in door_objs:
if obj.props.facing == 180:
if not pos:
pos.append(obj.props.xmp)
pos.append(obj.props.ymp)
else:
if obj.props.ymp > pos[1]:
room.door_locations[DoorCodes.DOOR_TOP_LEFT] = (pos[0], pos[1]) # tuple(pos)
room.door_locations[DoorCodes.DOOR_BOTTOM_LEFT] = (obj.props.xmp, obj.props.ymp)
else:
room.door_locations[DoorCodes.DOOR_TOP_LEFT] = (obj.props.xmp, obj.props.ymp)
room.door_locations[DoorCodes.DOOR_BOTTOM_LEFT] = (pos[0], pos[1]) # tuple(pos)
break
if right_doors == 1:
code = code | DoorCodes.DOOR_RIGHT
for obj in door_objs:
if obj.props.facing == 0:
room.door_locations[DoorCodes.DOOR_RIGHT] = (obj.props.xmp, obj.props.ymp)
break
elif right_doors == 2:
code = code | DoorCodes.DOOR_TOP_RIGHT | DoorCodes.DOOR_BOTTOM_RIGHT
# find top / bot
pos = []
for obj in door_objs:
if obj.props.facing == 0:
if not pos:
pos.append(obj.props.xmp)
pos.append(obj.props.ymp)
else:
if obj.props.ymp > pos[1]:
room.door_locations[DoorCodes.DOOR_TOP_RIGHT] = (pos[0], pos[1]) # tuple(pos)
room.door_locations[DoorCodes.DOOR_BOTTOM_RIGHT] = (obj.props.xmp, obj.props.ymp)
else:
room.door_locations[DoorCodes.DOOR_TOP_RIGHT] = (obj.props.xmp, obj.props.ymp)
room.door_locations[DoorCodes.DOOR_BOTTOM_RIGHT] = (pos[0], pos[1]) # tuple(pos)
break
room.door_codes = code
room.num_doors = len(door_objs)
def distUpdateLoop(self, room, prev_room): # recursively loops through room connections to update dist from main when a 'loop' is discovered
if room.dist_from_main > 20: # Should prevent recursion errors, at least temporarily
logging.info(f'Unusually high dist ({room.dist_from_main}): {room}')
return
for con in room.connections:
# "base case" is when all rooms are the dist +/- 1 or prev room
if con.next_room.id == prev_room.id: continue
if abs(con.next_room.dist_from_main - room.dist_from_main) > 1:
# bad dist detected, increase recursion level
con.next_room.dist_from_main = room.dist_from_main + 1
self.distUpdateLoop(con.next_room, room)
# def floydWarshall(self):
# rm_list = list(self.rooms)
# edges = [[1 << 32] * len(rm_list)] * len(rm_list)
# for i in range(len(rm_list)):
# for con in self.rooms[rm_list[i]].connections:
# con.next_room.id
def pathBackUpdateLoop(self, room):
# logging.info('---')
for con in room.connections:
# might not need prev id
# logging.info(f'room {room.id} dist: {room.dist_from_main}, next {con.next_room.id} dist: {con.next_room.dist_from_main}, {(con.next_room.dist_from_main == room.dist_from_main - 1)}')
if (con.next_room.id != 0) and (con.next_room.dist_from_main == room.dist_from_main - 1):
con.next_room.path_back = True
self.pathBackUpdateLoop(con.next_room)
def afterUpdate(self, inputs=None):
# return
cw = self.refs.ClientWorld
if cw == ffi.NULL: return
# Determine if the player is in the right map
zone = ffi.string(cw.asWorld.props.zone.s) or ffi.string(cw.asWorld.props.music.s)
if zone == b'sc_boss': self.initVals() # TODO: run once?
if zone != b'sc': return
# if zone != b'forest': return
if ffi.string(cw.asWorld.props.decoration.name.s) == b'sccore': return # we hate core rooms we hate core rooms, we hate core rooms we hate core rooms
# Get or create current room class
self.floor = cw.asWorld.props.floor
if self.floor not in self.rooms:
self.update_graphic = True
self.rooms[self.floor] = Room(self.floor)
self.identifyDoors(self.rooms[self.floor])
logging.info('\n/ new floor ----------------------------------------\\' +\
f'\n{self.prev_room} | {self.rooms[self.floor]}\n{self.prev_closest_door:06b} | {self.closest_door:06b}' +\
'\n\\---------------------------------------------------/')
self.room = self.rooms[self.floor]
self.decoration = ffi.string(cw.asWorld.props.decoration.name.s)
# Make sure the player exists
plr = cw.player
if plr == ffi.NULL: return # Player neds to load in before room changes and connections happen, takes a couple frames
pprops = ffi.cast('struct WorldObject *', plr).props
# Find the closest door
player_dist = 1 << 32 # very large int value, probably always larger than the player will be far
self.closest_door = DoorCodes.DOOR_NONE
for i in range(DoorCodes.NUM_DOOR):
door_code = 1 << i
if door_code & self.room.door_codes:
new_dist = dist(pprops.xmp, pprops.ymp, self.room.door_locations[door_code])
if new_dist < player_dist:
player_dist = new_dist
self.closest_door = door_code
# First time inits / connections
if not self.first_enter:
# Core rooms have an id of 0 but are distinguished by their decoration: b'sccore'
# main is b'scstart01'
# b'sctask01' is pat, b'sctask02' is hive, b'sctask03' is bones, b'sctask04' is jump
if self.prev_floor != self.floor: # make "handler" functions like `self.onRoomChange()`?
# logging.info('\n/ changed floor id ---------------------------------\\' +\
# f'\nEntered room #{self.floor} from #{self.prev_floor}' +\
# f'\ndecoration: {ffi.string(cw.asWorld.props.decoration.name.s)}' +\
# '\n\\---------------------------------------------------/')
# (next_room, this_room, next_code, this_code)
self.makeConnection()
self.room.color = self.config.current_room_color
if self.prev_room == self.main:
self.prev_room.color = self.config.main_room_color
elif self.prev_room.num_doors == 1:
self.prev_room.color = self.config.end_room_color
else:
self.prev_room.color = self.config.normal_room_color
if self.room.dist_from_main == -1:
self.room.dist_from_main = self.prev_room.dist_from_main + 1
# '''
elif not ((self.prev_room.dist_from_main == self.room.dist_from_main + 1) or (self.prev_room.dist_from_main == self.room.dist_from_main - 1)): # nor, can also use != + and (faster?)
# update back propagation triggered since previous room isnt current distance +1 or -1
# if self.room.dist_from_main > 5:
# return
if self.prev_room.dist_from_main < self.room.dist_from_main:
self.room.dist_from_main = self.prev_room.dist_from_main + 1
try:
self.distUpdateLoop(self.room, self.prev_room)
except RecursionError:
logging.error(f'Recursion Error distUpdateLoop\n{self.rooms}')
else:
self.prev_room.dist_from_main = self.room.dist_from_main + 1
try:
self.distUpdateLoop(self.prev_room, self.room)
except RecursionError:
logging.error(f'Recursion Error distUpdateLoop\n{self.rooms}')
# logging.info(f'2: {self.room.dist_from_main}, {self.prev_room.dist_from_main}')
# '''
for room in self.rooms: # maybe find a different way
self.rooms[room].path_back = False
try:
self.pathBackUpdateLoop(self.room)
except RecursionError:
logging.error(f'Recursion Error pathBackUpdateLoop\n{self.rooms}')
else:
self.first_enter = False
self.room.dist_from_main = 0
self.main = self.room
self.main.color = self.config.main_room_color
logging.info(f'First Enter, main: {self.main}')
# Set prevs
self.prev_floor = self.floor
self.prev_room = self.room
self.prev_closest_door = self.closest_door
# self.prev_decoration = self.decoration
self.d = player_dist
def graphicUpdateLoop(self, room, prev_room): # could also make a list of updated rooms instead of prev_room
if not room.dirty:
return
room.dirty = False
# logging.info(f'inside gUL room: {room.x}')
# if room.x > 5000:
# logging.error(f'recursion error; {room.x}')
# raise RecursionError
for con in room.connections:
if con.next_room.id != prev_room.id:
# logging.info('########################################################')
if con.this_code == DoorCodes.DOOR_LEFT:
con.next_room.x = room.x - (self.config.w + self.config.horizontal_spacing)
if con.next_code == DoorCodes.DOOR_TOP_RIGHT:
con.next_room.y = room.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.next_code == DoorCodes.DOOR_BOTTOM_RIGHT:
con.next_room.y = room.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
else:
con.next_room.y = room.y
elif con.this_code == DoorCodes.DOOR_RIGHT:
con.next_room.x = room.x + (self.config.w + self.config.horizontal_spacing)
if con.next_code == DoorCodes.DOOR_TOP_LEFT:
con.next_room.y = room.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.next_code == DoorCodes.DOOR_BOTTOM_LEFT:
con.next_room.y = room.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
else:
con.next_room.y = room.y
elif con.this_code == DoorCodes.DOOR_TOP_LEFT:
con.next_room.x = room.x - (self.config.w + self.config.horizontal_spacing)
con.next_room.y = room.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.this_code == DoorCodes.DOOR_BOTTOM_LEFT:
con.next_room.x = room.x - (self.config.w + self.config.horizontal_spacing)
con.next_room.y = room.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.this_code == DoorCodes.DOOR_TOP_RIGHT:
con.next_room.x = room.x + (self.config.w + self.config.horizontal_spacing)
con.next_room.y = room.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.this_code == DoorCodes.DOOR_BOTTOM_RIGHT:
con.next_room.x = room.x + (self.config.w + self.config.horizontal_spacing)
con.next_room.y = room.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
self.graphicUpdateLoop(con.next_room, room)
def draw(self, room):
self.refs.XDL_FillRect(room.x + self.config.x, room.y + self.config.y, self.config.w, self.config.h, room.color, lib.BLENDMODE_BLEND)
if room.path_back:
self.refs.XDL_FillRect(room.x + self.config.x + self.config.w // 4, room.y + self.config.y + self.config.h // 4, self.config.w // 2, self.config.h // 2, self.config.current_room_color, lib.BLENDMODE_BLEND)
def onPresent(self, inputs=None):
if not self.rooms: return
# door code key: left, right, top_left, bottom_left, top_right, bottom_right
while self.update_graphic and not self.first_enter:
self.update_graphic = False
# logging.info(f'before gUL main: {self.main.x}')
for floor_id in self.rooms:
self.rooms[floor_id].dirty = True
self.main.dirty = False
for con in self.main.connections:
# TODO: set room pos, set connection pos, create guess connections
if con.this_code == DoorCodes.DOOR_LEFT:
con.next_room.x = self.main.x - (self.config.w + self.config.horizontal_spacing)
if con.next_code == DoorCodes.DOOR_TOP_RIGHT:
con.next_room.y = self.main.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.next_code == DoorCodes.DOOR_BOTTOM_RIGHT:
con.next_room.y = self.main.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
else:
con.next_room.y = self.main.y
elif con.this_code == DoorCodes.DOOR_RIGHT:
con.next_room.x = self.main.x + (self.config.w + self.config.horizontal_spacing)
if con.next_code == DoorCodes.DOOR_TOP_LEFT:
con.next_room.y = self.main.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.next_code == DoorCodes.DOOR_BOTTOM_LEFT:
con.next_room.y = self.main.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
else:
con.next_room.y = self.main.y
elif con.this_code == DoorCodes.DOOR_TOP_LEFT:
con.next_room.x = self.main.x - (self.config.w + self.config.horizontal_spacing)
con.next_room.y = self.main.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.this_code == DoorCodes.DOOR_BOTTOM_LEFT:
con.next_room.x = self.main.x - (self.config.w + self.config.horizontal_spacing)
con.next_room.y = self.main.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.this_code == DoorCodes.DOOR_TOP_RIGHT:
con.next_room.x = self.main.x + (self.config.w + self.config.horizontal_spacing)
con.next_room.y = self.main.y - (self.config.w // 2 + self.config.vertical_spacing // 2)
elif con.this_code == DoorCodes.DOOR_BOTTOM_RIGHT:
con.next_room.x = self.main.x + (self.config.w + self.config.horizontal_spacing)
con.next_room.y = self.main.y + (self.config.w // 2 + self.config.vertical_spacing // 2)
try:
self.graphicUpdateLoop(con.next_room, self.main)
except RecursionError:
logging.error(f'Recursion Error graphicUpdateLoop\n{self.rooms}')
if self.config.show_map: # put map update here?
for floor in self.rooms:
cw = self.refs.canvasW_[0]
ch = self.refs.canvasH_[0]
self.refs.canvasW_[0] = self.refs.windowW
self.refs.canvasH_[0] = self.refs.windowH
self.draw(self.rooms[floor])
self.refs.canvasW_[0] = cw
self.refs.canvasH_[0] = ch
if self.config.show_debug:
text = ''
for room in self.rooms:
text += str(self.rooms[room]) + '\n'
self.debug_txt.text = text + f'\n{self.closest_door:06b}: {self.d}\n{self.decoration}, {self.prev_room.dist_from_main}'
# self.debug_txt.text = f'{self.room}\n{self.prev_room}'
self.debug_txt.draw(300, 300)
[plugin_sc_map]
show_map = yes
show_debug = no
x = 350
y = 250
w = 50
h = 50
horizontal_spacing = 25
vertical_spacing = 25
current_room_color = 80ff8080
main_room_color = 800080ff
normal_room_color = 80ff80ff
end_room_color = 8080ff00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment