Skip to content

Commit

Permalink
MultiServer: allow multiple, ordered operations
Browse files Browse the repository at this point in the history
MultiServer: rename "data" on Get, Retrieved and SetNotify to "keys"
MultiServer: add some more operators
SniClient: some pep8 cleanup
  • Loading branch information
Berserker66 committed Mar 4, 2022
1 parent 2c3e3f0 commit 7ca6f24
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 54 deletions.
9 changes: 5 additions & 4 deletions FactorioClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,17 +169,18 @@ async def game_watcher(ctx: FactorioContext):
# attempt to refill
ctx.last_deplete = time.time()
asyncio.create_task(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operation": "deplete",
"value": -ctx.energy_link_increment * in_world_bridges,
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
{"operation": "max", "value": 0}],
"last_deplete": ctx.last_deplete
}]))
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
ctx.energy_link_increment*in_world_bridges:
value = ctx.energy_link_increment * in_world_bridges
asyncio.create_task(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operation": "add",
"value": value
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": value}]
}]))
ctx.rcon_client.send_command(
f"/ap-energylink -{value}")
Expand Down
31 changes: 20 additions & 11 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,20 @@

# functions callable on storable data on the server by clients
modify_functions = {
"add": operator.add,
"add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append)
"mul": operator.mul,
"mod": operator.mod,
"max": max,
"min": min,
"replace": lambda old, new: new,
"deplete": lambda value, change: max(0, value + change)
"default": lambda old, new: old,
"pow": operator.pow,
# bitwise:
"xor": operator.xor,
"or": operator.or_,
"and": operator.and_,
"left_shift": operator.lshift,
"right_shift": operator.rshift,
}


Expand Down Expand Up @@ -1544,26 +1552,27 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
await ctx.send_encoded_msgs(bounceclient, msg)

elif cmd == "Get":
if "data" not in args or type(args["data"]) != list:
if "keys" not in args or type(args["keys"]) != list:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
"text": 'Retrieve', "original_cmd": cmd}])
return
args["cmd"] = "Retrieved"
keys = args["data"]
args["data"] = {key: ctx.stored_data.get(key, None) for key in keys}
keys = args["keys"]
args["keys"] = {key: ctx.stored_data.get(key, None) for key in keys}
await ctx.send_msgs(client, [args])

elif cmd == "Set":
if "key" not in args or "value" not in args:
if "key" not in args or "value" not in args or \
"operations" not in args or not type(args["operations"]) == list:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
"text": 'Set', "original_cmd": cmd}])
return
args["cmd"] = "SetReply"
value = ctx.stored_data.get(args["key"], args.get("default", 0))
args["original_value"] = value
operation = args.get("operation", "replace")
func = modify_functions[operation]
value = func(value, args.get("value"))
for operation in args["operations"]:
func = modify_functions[operation["operation"]]
value = func(value, operation["value"])
ctx.stored_data[args["key"]] = args["value"] = value
targets = set(ctx.stored_data_notification_clients[args["key"]])
if args.get("want_reply", True):
Expand All @@ -1572,11 +1581,11 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.broadcast(targets, [args])

elif cmd == "SetNotify":
if "data" not in args or type(args["data"]) != list:
if "keys" not in args or type(args["keys"]) != list:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
"text": 'SetNotify', "original_cmd": cmd}])
return
for key in args["data"]:
for key in args["keys"]:
ctx.stored_data_notification_clients[key].add(client)


Expand Down
92 changes: 53 additions & 39 deletions SNIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
import logging
import asyncio
from json import loads, dumps
from tkinter import font

from Utils import get_item_name_from_id, init_logging
from Utils import init_logging

if __name__ == "__main__":
init_logging("SNIClient", exception_logger="Client")
Expand All @@ -22,14 +21,12 @@

from NetUtils import *
from worlds.alttp import Regions, Shops
from worlds.alttp import Items
from worlds.alttp.Rom import ROM_PLAYER_LIMIT
from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
import Utils
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser
from Patch import GAME_ALTTP, GAME_SM


snes_logger = logging.getLogger("SNES")

from MultiServer import mark_raw
Expand All @@ -41,7 +38,7 @@ class DeathState(enum.IntEnum):
dead = 3


class LttPCommandProcessor(ClientCommandProcessor):
class SNIClientCommandProcessor(ClientCommandProcessor):
ctx: Context

def _cmd_slow_mode(self, toggle: str = ""):
Expand Down Expand Up @@ -72,7 +69,6 @@ def _cmd_snes(self, snes_options: str = "") -> bool:
snes_address = options[0]
snes_device_number = int(options[1])


self.ctx.snes_reconnect_address = None
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number), name="SNES Connect")
return True
Expand All @@ -92,15 +88,23 @@ def _cmd_snes_close(self) -> bool:
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
# self.output("No attached SNES Device.")
# return False
#
# snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)]))
# asyncio.create_task(snes_flush_writes(self.ctx))
# self.output("Data Sent")
# return True

# def _cmd_snes_read(self, address, size=1):
# """Read the SNES' memory address (base16)."""
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
# self.output("No attached SNES Device.")
# return False
# data = await snes_read(self.ctx, int(address, 16), size)
# self.output(f"Data Read: {data}")
# return True


class Context(CommonContext):
command_processor = LttPCommandProcessor
command_processor = SNIClientCommandProcessor
game = "A Link to the Past"
items_handling = None # set in game_watcher

Expand Down Expand Up @@ -183,7 +187,8 @@ async def deathlink_kill_player(ctx: Context):
continue
if not invincible[0] and last_health[0] == health[0]:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
snes_buffered_write(ctx, WRAM_START + 0x0373,
bytes([8])) # deal 1 full heart of damage at next opportunity
elif ctx.game == GAME_SM:
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
if not ctx.death_link_allow_survive:
Expand All @@ -200,7 +205,8 @@ async def deathlink_kill_player(ctx: Context):
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
if health is not None:
health = health[0] | (health[1] << 8)
if not gamemode or gamemode[0] in SM_DEATH_MODES or (ctx.death_link_allow_survive and health is not None and health > 0):
if not gamemode or gamemode[0] in SM_DEATH_MODES or (
ctx.death_link_allow_survive and health is not None and health > 0):
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()

Expand Down Expand Up @@ -914,7 +920,7 @@ async def game_watcher(ctx: Context):

ctx.rom = rom
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
Expand Down Expand Up @@ -976,7 +982,8 @@ async def game_watcher(ctx: Context):
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
color(ctx.item_name_getter(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))

snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
Expand All @@ -998,7 +1005,7 @@ async def game_watcher(ctx: Context):
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
await track_locations(ctx, roomid, roomdata)
elif ctx.game == GAME_SM:
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
Expand All @@ -1025,14 +1032,16 @@ async def game_watcher(ctx: Context):
itemIndex = (message[4] | (message[5] << 8)) >> 3

recv_index += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))

from worlds.sm.Locations import locations_start_id
location_id = locations_start_id + itemIndex

ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id)
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])

data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
Expand All @@ -1048,11 +1057,14 @@ async def game_watcher(ctx: Context):
itemId = item.item - items_start_id

playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes([playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF]))
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes(
[playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF]))
itemOutPtr += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602,
bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
color(ctx.item_name_getter(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)

Expand Down Expand Up @@ -1130,6 +1142,7 @@ async def main():
if input_task:
input_task.cancel()


def get_alttp_settings(romfile: str):
lastSettings = Utils.get_adjuster_settings(GAME_ALTTP)
adjusted = False
Expand All @@ -1139,8 +1152,8 @@ def get_alttp_settings(romfile: str):
if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply:

whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
"reduceflashing", "deathlink"}
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
"reduceflashing", "deathlink"}
printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist}
if hasattr(lastSettings, "sprite_pool"):
sprite_pool = {}
Expand All @@ -1154,40 +1167,41 @@ def get_alttp_settings(romfile: str):
import pprint

if gui_enabled:

from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button
applyPromptWindow = Tk()
applyPromptWindow.resizable(False, False)
applyPromptWindow.protocol('WM_DELETE_WINDOW',lambda: onButtonClick())
applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick())
logo = PhotoImage(file=Utils.local_path('data', 'icon.png'))
applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo)
applyPromptWindow.wm_title("Last adjuster settings LttP")

label = LabelFrame(applyPromptWindow,
text='Last used adjuster settings were found. Would you like to apply these?')
label.grid(column=0,row=0, padx=5, pady=5, ipadx=5, ipady=5)
label.grid_columnconfigure (0, weight=1)
label.grid_columnconfigure (1, weight=1)
label.grid_columnconfigure (2, weight=1)
label.grid_columnconfigure (3, weight=1)
def onButtonClick(answer: str='no'):
text='Last used adjuster settings were found. Would you like to apply these?')
label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5)
label.grid_columnconfigure(0, weight=1)
label.grid_columnconfigure(1, weight=1)
label.grid_columnconfigure(2, weight=1)
label.grid_columnconfigure(3, weight=1)

def onButtonClick(answer: str = 'no'):
setattr(onButtonClick, 'choice', answer)
applyPromptWindow.destroy()

framedOptions = Frame(label)
framedOptions.grid(column=0, columnspan=4,row=0)
framedOptions.grid(column=0, columnspan=4, row=0)
framedOptions.grid_columnconfigure(0, weight=1)
framedOptions.grid_columnconfigure(1, weight=1)
framedOptions.grid_columnconfigure(2, weight=1)
curRow = 0
curCol = 0
for name, value in printed_options.items():
Label(framedOptions, text=name+": "+str(value)).grid(column=curCol, row=curRow, padx=5)
if(curCol==2):
curRow+=1
curCol=0
Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5)
if (curCol == 2):
curRow += 1
curCol = 0
else:
curCol+=1
curCol += 1

yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10)
yesButton.grid(column=0, row=1)
Expand All @@ -1203,8 +1217,8 @@ def onButtonClick(answer: str='no'):
choice = getattr(onButtonClick, 'choice')
else:
choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
f"{pprint.pformat(printed_options)}\n"
f"Enter yes, no, always or never: ")
f"{pprint.pformat(printed_options)}\n"
f"Enter yes, no, always or never: ")
if choice and choice.startswith("y"):
choice = 'yes'
elif choice and "never" in choice:
Expand All @@ -1221,7 +1235,7 @@ def onButtonClick(answer: str='no'):
choice = 'no'
elif 'always' in lastSettings.auto_apply:
choice = 'yes'

if 'yes' in choice:
from worlds.alttp.Rom import get_base_rom_path
lastSettings.rom = romfile
Expand All @@ -1247,10 +1261,10 @@ def onButtonClick(answer: str='no'):
except Exception as e:
logging.exception(e)
else:

adjusted = False
return adjustedromfile, adjusted


if __name__ == '__main__':
colorama.init()
loop = asyncio.get_event_loop()
Expand Down

0 comments on commit 7ca6f24

Please sign in to comment.