From f10e239096d37bc5e1740f1b40f5e09277215598 Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 29 Oct 2018 21:57:29 +0000 Subject: [PATCH] Added core library files --- README.md | 17 +- Steps.py | 70 - chart.png => pics/chart.png | Bin hogwild3.gif => pics/hogwild3.gif | Bin src/emulator/Action.py | 8 + src/emulator/Console.py | 83 + src/emulator/Emulator.py | 131 + src/emulator/StreamGobbler.py | 32 + src/emulator/mame/cfg/sfiii3n.cfg | 8 + src/emulator/mame/history/history.db | Bin 0 -> 12288 bytes src/emulator/mame/plugins/README.md | 3 + src/emulator/mame/plugins/boot.lua | 52 + .../mame/plugins/cheat/cheat_json.lua | 12 + .../mame/plugins/cheat/cheat_simple.lua | 321 + src/emulator/mame/plugins/cheat/cheat_xml.lua | 251 + src/emulator/mame/plugins/cheat/init.lua | 868 + src/emulator/mame/plugins/cheat/plugin.json | 10 + .../mame/plugins/cheat/xml_to_json.lua | 11 + src/emulator/mame/plugins/cheatfind/init.lua | 825 + .../mame/plugins/cheatfind/plugin.json | 10 + src/emulator/mame/plugins/console/init.lua | 254 + src/emulator/mame/plugins/console/plugin.json | 10 + .../mame/plugins/data/button_char.lua | 245 + .../mame/plugins/data/data_command.lua | 30 + .../mame/plugins/data/data_gameinit.lua | 27 + .../mame/plugins/data/data_hiscore.lua | 828 + .../mame/plugins/data/data_history.lua | 26 + .../mame/plugins/data/data_mameinfo.lua | 31 + src/emulator/mame/plugins/data/data_marp.lua | 151 + .../mame/plugins/data/data_messinfo.lua | 32 + src/emulator/mame/plugins/data/data_story.lua | 33 + .../mame/plugins/data/data_sysinfo.lua | 26 + src/emulator/mame/plugins/data/database.lua | 37 + src/emulator/mame/plugins/data/init.lua | 81 + src/emulator/mame/plugins/data/load_dat.lua | 169 + src/emulator/mame/plugins/data/plugin.json | 10 + src/emulator/mame/plugins/dummy/init.lua | 33 + src/emulator/mame/plugins/dummy/plugin.json | 10 + src/emulator/mame/plugins/gdbstub/init.lua | 289 + src/emulator/mame/plugins/gdbstub/plugin.json | 10 + src/emulator/mame/plugins/hiscore/hiscore.dat | 17719 ++++++++++++++++ src/emulator/mame/plugins/hiscore/init.lua | 309 + src/emulator/mame/plugins/hiscore/plugin.json | 10 + .../mame/plugins/hiscore/sort_hiscore.lua | 217 + src/emulator/mame/plugins/json/LICENSE | 22 + src/emulator/mame/plugins/json/README.md | 2 + src/emulator/mame/plugins/json/init.lua | 735 + src/emulator/mame/plugins/json/plugin.json | 9 + src/emulator/mame/plugins/layout/init.lua | 50 + src/emulator/mame/plugins/layout/plugin.json | 10 + src/emulator/mame/plugins/portname/init.lua | 161 + .../mame/plugins/portname/plugin.json | 10 + src/emulator/mame/plugins/timer/init.lua | 109 + src/emulator/mame/plugins/timer/plugin.json | 10 + src/emulator/pipes/Address.py | 19 + src/emulator/pipes/DataPipe.py | 36 + src/emulator/pipes/Pipe.py | 99 + Actions.py => src/sf_environment/Actions.py | 2 +- .../sf_environment/Environment.py | 98 +- src/sf_environment/Steps.py | 69 + test/TestRunner.py | 38 + test/emulator/ConsoleTest.py | 43 + test/emulator/EmulatorTest.py | 55 + test/emulator/StreamGobblerTest.py | 63 + test/emulator/pipes/AddressTest.py | 19 + test/emulator/pipes/DataPipeTest.py | 104 + test/emulator/pipes/PipeTest.py | 175 + 67 files changed, 25115 insertions(+), 122 deletions(-) delete mode 100644 Steps.py rename chart.png => pics/chart.png (100%) rename hogwild3.gif => pics/hogwild3.gif (100%) create mode 100755 src/emulator/Action.py create mode 100755 src/emulator/Console.py create mode 100755 src/emulator/Emulator.py create mode 100755 src/emulator/StreamGobbler.py create mode 100644 src/emulator/mame/cfg/sfiii3n.cfg create mode 100644 src/emulator/mame/history/history.db create mode 100755 src/emulator/mame/plugins/README.md create mode 100755 src/emulator/mame/plugins/boot.lua create mode 100755 src/emulator/mame/plugins/cheat/cheat_json.lua create mode 100755 src/emulator/mame/plugins/cheat/cheat_simple.lua create mode 100755 src/emulator/mame/plugins/cheat/cheat_xml.lua create mode 100755 src/emulator/mame/plugins/cheat/init.lua create mode 100755 src/emulator/mame/plugins/cheat/plugin.json create mode 100755 src/emulator/mame/plugins/cheat/xml_to_json.lua create mode 100755 src/emulator/mame/plugins/cheatfind/init.lua create mode 100755 src/emulator/mame/plugins/cheatfind/plugin.json create mode 100755 src/emulator/mame/plugins/console/init.lua create mode 100755 src/emulator/mame/plugins/console/plugin.json create mode 100755 src/emulator/mame/plugins/data/button_char.lua create mode 100755 src/emulator/mame/plugins/data/data_command.lua create mode 100755 src/emulator/mame/plugins/data/data_gameinit.lua create mode 100755 src/emulator/mame/plugins/data/data_hiscore.lua create mode 100755 src/emulator/mame/plugins/data/data_history.lua create mode 100755 src/emulator/mame/plugins/data/data_mameinfo.lua create mode 100755 src/emulator/mame/plugins/data/data_marp.lua create mode 100755 src/emulator/mame/plugins/data/data_messinfo.lua create mode 100755 src/emulator/mame/plugins/data/data_story.lua create mode 100755 src/emulator/mame/plugins/data/data_sysinfo.lua create mode 100755 src/emulator/mame/plugins/data/database.lua create mode 100755 src/emulator/mame/plugins/data/init.lua create mode 100755 src/emulator/mame/plugins/data/load_dat.lua create mode 100755 src/emulator/mame/plugins/data/plugin.json create mode 100755 src/emulator/mame/plugins/dummy/init.lua create mode 100755 src/emulator/mame/plugins/dummy/plugin.json create mode 100755 src/emulator/mame/plugins/gdbstub/init.lua create mode 100755 src/emulator/mame/plugins/gdbstub/plugin.json create mode 100755 src/emulator/mame/plugins/hiscore/hiscore.dat create mode 100755 src/emulator/mame/plugins/hiscore/init.lua create mode 100755 src/emulator/mame/plugins/hiscore/plugin.json create mode 100755 src/emulator/mame/plugins/hiscore/sort_hiscore.lua create mode 100755 src/emulator/mame/plugins/json/LICENSE create mode 100755 src/emulator/mame/plugins/json/README.md create mode 100755 src/emulator/mame/plugins/json/init.lua create mode 100755 src/emulator/mame/plugins/json/plugin.json create mode 100755 src/emulator/mame/plugins/layout/init.lua create mode 100755 src/emulator/mame/plugins/layout/plugin.json create mode 100755 src/emulator/mame/plugins/portname/init.lua create mode 100755 src/emulator/mame/plugins/portname/plugin.json create mode 100755 src/emulator/mame/plugins/timer/init.lua create mode 100755 src/emulator/mame/plugins/timer/plugin.json create mode 100755 src/emulator/pipes/Address.py create mode 100755 src/emulator/pipes/DataPipe.py create mode 100755 src/emulator/pipes/Pipe.py rename Actions.py => src/sf_environment/Actions.py (97%) mode change 100644 => 100755 rename Environment.py => src/sf_environment/Environment.py (74%) create mode 100644 src/sf_environment/Steps.py create mode 100755 test/TestRunner.py create mode 100755 test/emulator/ConsoleTest.py create mode 100755 test/emulator/EmulatorTest.py create mode 100755 test/emulator/StreamGobblerTest.py create mode 100755 test/emulator/pipes/AddressTest.py create mode 100755 test/emulator/pipes/DataPipeTest.py create mode 100755 test/emulator/pipes/PipeTest.py diff --git a/README.md b/README.md index d19a3d5..b730f72 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This Python library has the to potential to train your reinforcement learning al The toolkit has currently been applied to Street Fighter III Third Strike: Fight for the Future, but can modified for any game available on MAME. The following demonstrates how a random agent can be written for a street fighter environment. ```python import random -from Main.SF_Environment.Environment import Environment +from sf_environment.Environment import Environment env = Environment(difficulty=3, frame_ratio=3, frames_per_step=3) env.start() @@ -25,7 +25,7 @@ The toolkit also supports hogwild training: ```Python from threading import Thread import random -from Main.SF_Environment.Environment import Environment +from src.sf_environment.Environment import Environment def run_env(env): @@ -48,14 +48,15 @@ def main(): [thread.start() for thread in threads] ``` -![](https://raw.githubusercontent.com/BombayCinema/MAMEToolkit/master/hogwild3.gif "Hogwild Random Agents") +![](pics/hogwild3.gif "Hogwild Random Agents") ## Setting Up Your Own Game Environment It doesn't take much to interact with the emulator itself using the toolkit, however the challenge comes from finding the memory address values associated with the internal state you care about, and tracking said state with your environment class. The internal memory states of a game can be tracked using the [MAME Cheat Debugger](http://docs.mamedev.org/debugger/cheats.html), which allows you to track how the memory address values of the game change over time. To create an emulation of the game you must first have the ROM for the game you are emulating and know the game ID used by MAME, for example for this version of street fighter it is 'sfiii3n'. Once you have these and have determined the memory addresses you wish to track you can start the emulation: ```python -from MAMEToolkit import Emulator +from emulator.Emulator import Emulator +from emulator.pipes.Address import Address game_id = "sfiii3n" memory_addresses = { @@ -83,14 +84,14 @@ The step function returns the frame data as a NumPy matrix, along with all of th To send actions to the emulator you also need to determine which input ports and fields the game supports. For example, with street fighter to insert a coin the following code is required: ```python -from MAMEToolkit import Action +from emulator.Action import Action insert_coin = Action(':INPUTS', 'Coin 1') data = emulator.step([insert_coin]) ``` To identify which ports are availble use the list actions command: ```python -from MAMEToolkit import list_actions +from emulator.Emulator import list_actions game_id = "sfiii3n" print(list_actions(game_id)) @@ -133,7 +134,7 @@ There is also the problem of transitioning games between non-learnable gameplay The emulator class also has a frame_ratio argument which can be used for adjusting the frame rate seen by your algorithm. By default MAME generates frames at 60 frames per second, however, this may be too many frames for your algorithm. The toolkit by default will use a frame_ratio of 3, which means that 1 in 3 frames are sent through the toolkit, this converts the frame rate to 20 frames per second. Using a higher frame_ratio also increases the performance of the toolkit. ```Python -from MAMEToolkit import Emulator +from emulator.Emulator import Emulator emulator = Emulator("sfiii3n", memory_addresses, frame_ratio=3) ``` @@ -145,6 +146,6 @@ With a single random agent, the street fighter environment can be run at 600%+ t ## Simple ConvNet Agent To ensure that the toolkit is able to train algorithms, a simple 5 layer ConvNet was setup with minimal tuning. The algorithm was able to successfully learn some simple mechanics of Street Fighter, such as combos and blocking. The Street Fighter gameplay works by having the player fight different opponents across 10 stages of increasing difficulty. Initially, the algorithm would reach stage 2 on average, but eventually could reach stage 5 on average after 2200 episodes of training. The learning rate was tracked using the net damage done vs damage taken of a single playthough for each episode. -![](https://raw.githubusercontent.com/BombayCinema/MAMEToolkit/master/chart.png "ConvNet Results") +![](pics/chart.png "ConvNet Results") diff --git a/Steps.py b/Steps.py deleted file mode 100644 index 9439684..0000000 --- a/Steps.py +++ /dev/null @@ -1,70 +0,0 @@ -from Main.SF_Environment.Actions import Actions - - -def set_difficulty(frame_ratio, difficulty): - steps = [ - {"wait": 0, "actions": [Actions.SERVICE]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_JPUNCH]}] - if (difficulty % 8) < 3: - steps += [{"wait": int(30/frame_ratio), "actions": [Actions.P1_LEFT]} for i in range(3-(difficulty % 8))] - else: - steps += [{"wait": int(30/frame_ratio), "actions": [Actions.P1_RIGHT]} for i in range((difficulty % 8)-3)] - steps += [ - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_DOWN]}, - {"wait": int(30/frame_ratio), "actions": [Actions.P1_JPUNCH]}] - return steps - - -def start_game(frame_ratio): - return [ - {"wait": 180, "actions": [Actions.COIN_P1]}, - {"wait": 1, "actions": [Actions.COIN_P1]}, - {"wait": int(60/frame_ratio), "actions": [Actions.P1_START]}, - {"wait": int(80/frame_ratio), "actions": [Actions.P1_LEFT, Actions.P1_JPUNCH]}, - {"wait": int(60/frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": int(60/frame_ratio), "actions": [Actions.P1_JPUNCH]}] - - -def reset_game(frame_ratio, wins): - steps = [] - if wins["P1"] == 2: - steps += [ - {"wait": int(60/frame_ratio), "actions": [Actions.P1_JPUNCH]} - ] - steps += [{"wait": 0, "actions": [Actions.P1_JPUNCH]} for i in range(int(180/frame_ratio))] - steps += [{"wait": int(60/frame_ratio), "actions": [Actions.P1_JPUNCH]}] - elif wins["P2"] == 2: - steps += [{"wait": 0, "actions": [Actions.SERVICE]}, - {"wait": int(30 / frame_ratio), "actions": [Actions.P1_UP]}, - {"wait": int(30 / frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": 180, "actions": [Actions.COIN_P1]}, - {"wait": 1, "actions": [Actions.COIN_P1]}, - {"wait": int(60 / frame_ratio), "actions": [Actions.P1_START]}, - {"wait": int(80 / frame_ratio), "actions": [Actions.P1_LEFT, Actions.P1_JPUNCH]}, - {"wait": int(60 / frame_ratio), "actions": [Actions.P1_JPUNCH]}, - {"wait": int(60 / frame_ratio), "actions": [Actions.P1_JPUNCH]}] - else: - raise EnvironmentError("ERROR: Asked to restart gameplay without winner") - return steps diff --git a/chart.png b/pics/chart.png similarity index 100% rename from chart.png rename to pics/chart.png diff --git a/hogwild3.gif b/pics/hogwild3.gif similarity index 100% rename from hogwild3.gif rename to pics/hogwild3.gif diff --git a/src/emulator/Action.py b/src/emulator/Action.py new file mode 100755 index 0000000..cc1451e --- /dev/null +++ b/src/emulator/Action.py @@ -0,0 +1,8 @@ +class Action(object): + + def __init__(self, port, field): + self.port = port + self.field = field + + def get_lua_string(self): + return 'iop.ports["' + self.port + '"].fields["' + self.field + '"]' diff --git a/src/emulator/Console.py b/src/emulator/Console.py new file mode 100755 index 0000000..a491f91 --- /dev/null +++ b/src/emulator/Console.py @@ -0,0 +1,83 @@ +import os +from subprocess import Popen, PIPE +from src.emulator.StreamGobbler import StreamGobbler +import queue +import logging + + +# A class for starting the MAME emulator, and communicating with the Lua engine console +class Console(object): + + # Starts up an instance of MAME with POpen + # Uses a separate thread for reading from the console outputs + # render is for displaying the frames to the emulator window, disabling it has little to no effect + # throttle enabled will run any game at the intended gameplay speed, disabling it will run the game as fast as the computer can handle + # debug enabled will print everything that comes out of the Lua engine console + def __init__(self, game_id, render=True, throttle=False, debug=False): + self.logger = logging.getLogger("Console") + + command = "exec ./mame -rompath roms -pluginspath plugins -skip_gameinfo -sound none -console "+game_id + if not render: + command += " -video none" + if throttle: + command += " -throttle" + else: + command += " -frameskip 10" + + # Start lua console + script_path = os.path.dirname(os.path.abspath(__file__)) + self.process = Popen(command, cwd=f"{script_path}/mame", shell=True, stdin=PIPE, stdout=PIPE) + + # Start read queues + self.stdout_queue = queue.Queue() + self.gobbler = StreamGobbler(self.process.stdout, self.stdout_queue, debug=debug) + self.gobbler.wait_for_cursor() + self.gobbler.start() + + # Read the oldest line which may have been output by the console + # Uses the FIFO principle, once a line is read it is removed from the queue + # timeout determines how long the function will wait for an output if there is nothing immediately available + def readln(self, timeout=0.5): + line = self.stdout_queue.get(timeout=timeout) + while len(line)>0 and line[0] == 27: + line = line[19:] + return line.decode("utf-8") + + # Read as many lines from the console as there are available + # timeout determines how long the function will wait for an output if there is nothing immediately available + def readAll(self, timeout=0.5): + lines = [] + while True: + try: + lines.append(self.readln(timeout=timeout)) + except queue.Empty as e: + break + return lines + + def writeln(self, command, expect_output=False, timeout=0.5): + self.process.stdin.write(command.encode("utf-8") + b'\n') + self.process.stdin.flush() + output = self.readAll(timeout=timeout) + + if expect_output and len(output) == 0: + error = "Expected output but received nothing from emulator after '" + command + "'" + self.logger.error(error) + raise IOError(error) + if not expect_output and len(output) > 0: + error = "No output expected from command '" + command + "', but recieved: " + "\n".join(output) + self.logger.error(error) + raise IOError(error) + if expect_output: + return output + + # Mainly for testing + # Safely kills the emulator process + def close(self): + self.process.kill() + try: + self.process.wait(timeout=3) + except Exception as e: + error = "Failed to close emulator console" + self.logger.error(error, e) + raise EnvironmentError(error) + self.gobbler.stop() diff --git a/src/emulator/Emulator.py b/src/emulator/Emulator.py new file mode 100755 index 0000000..3568232 --- /dev/null +++ b/src/emulator/Emulator.py @@ -0,0 +1,131 @@ +import atexit +import os +from src.emulator.Console import Console +from src.emulator.pipes.Pipe import Pipe +from src.emulator.pipes.DataPipe import DataPipe + + +# Converts a list of action Enums into the relevant Lua engine representation +def actions_to_string(actions): + action_strings = [action.get_lua_string() for action in actions] + return '+'.join(action_strings) + + +def list_actions(game_id): + console = Console(game_id) + console.writeln('iop = manager:machine():ioport()') + actions = [] + ports = console.writeln("for k,v in pairs(iop.ports) do print(k) end", expect_output=True, timeout=0.5) + for port in ports: + fields = console.writeln("for k,v in pairs(iop.ports['"+port+"'].fields) do print(k) end", expect_output=True) + for field in fields: + actions.append({"port": port, "field": field}) + console.close() + return actions + + +# An interface for using the Lua engine console functionality +class Emulator(object): + + # env_id - the unique id of the emulator + # game_id - the game id being used + # memory_addresses - The internal memory addresses of the game which this class will return the value of at every time step + # frame_ratio - the ratio of frames that will be returned, 3 means 1 out of every 3 frames will be returned. Note that his also effects how often memory addresses are read and actions are sent + # See console for render, throttle & debug + def __init__(self, env_id, game_id, memory_addresses, frame_ratio=3, render=True, throttle=False, debug=False): + self.memoryAddresses = memory_addresses + self.frameRatio = frame_ratio + + # setup lua engine + self.console = Console(game_id, render=render, throttle=throttle, debug=debug) + atexit.register(self.close) + self.wait_for_resource_registration() + self.create_lua_variables() + screen_width = self.setup_screen_width() + screen_height = self.setup_screen_height() + self.screenDims = {"width": screen_width, "height": screen_height} + + # open pipes + pipes_path = f"{os.path.dirname(os.path.abspath(__file__))}/mame/pipes" + self.actionPipe = Pipe(env_id, "action", 'w', pipes_path) + self.actionPipe.open(self.console) + + self.dataPipe = DataPipe(env_id, self.screenDims, memory_addresses, pipes_path) + self.dataPipe.open(self.console) + + # Connect inter process communication + self.setup_frame_access_loop() + + def create_lua_variables(self): + self.console.writeln('iop = manager:machine():ioport()') + self.console.writeln('s = manager:machine().screens[":screen"]') + self.console.writeln('mem = manager:machine().devices[":maincpu"].spaces["program"]') + self.console.writeln('releaseQueue = {}') + + def wait_for_resource_registration(self): + screen_registered = False + program_registered = False + while not screen_registered or not program_registered: + if not screen_registered: + screen_registered = self.console.writeln('print(manager:machine().screens[":screen"])', expect_output=True, timeout=3) is not "nil" + if not program_registered: + program_registered = self.console.writeln('print(manager:machine().devices[":maincpu"].spaces["program"])', expect_output=True, timeout=3) is not "nil" + + # Gets the game screen width in pixels + def setup_screen_width(self): + output = self.console.writeln('print(s:width())', expect_output=True, timeout=1) + if len(output) != 1: + raise IOError('Expected one result from "print(s:width())", but received: ', output) + return int(output[0]) + + # Gets the game screen height in pixels + def setup_screen_height(self): + output = self.console.writeln('print(s:height())', expect_output=True, timeout=1) + if len(output) != 1: + raise IOError('Expected one result from "print(s:height())"", but received: ', output) + return int(output[0]) + + # Pauses the emulator + def pause_game(self): + self.console.writeln('emu.pause()') + + # Unpauses the emulator + def unpause_game(self): + self.console.writeln('emu.unpause()') + + # Sets up the callback function written in Lua that the Lua engine will execute each time a frame done + def setup_frame_access_loop(self): + pipe_data_func = 'function pipeData() ' \ + 'if (math.fmod(tonumber(s:frame_number()),' + str(self.frameRatio) +') == 0) then ' \ + 'for i=1,#releaseQueue do ' \ + 'releaseQueue[i](); ' \ + 'releaseQueue[i]=nil; ' \ + 'end; ' \ + '' + self.dataPipe.get_lua_string() + '' \ + 'actions = ' + self.actionPipe.get_lua_string() + '' \ + 'if (string.len(actions) > 1) then ' \ + 'for action in string.gmatch(actions, "[^+]+") do ' \ + 'actionFunc = loadstring(action..":set_value(1)"); ' \ + 'actionFunc(); ' \ + 'releaseFunc = loadstring(action..":set_value(0)"); ' \ + 'table.insert(releaseQueue, releaseFunc); ' \ + 'end; ' \ + 'end; ' \ + 'end; ' \ + 'end' + self.console.writeln(pipe_data_func) + self.console.writeln('emu.register_frame_done(pipeData, "data")') + + # Steps the emulator along one time step + def step(self, actions): + data = self.dataPipe.read_data(timeout=10) # gathers the frame data and memory address values + action_string = actions_to_string(actions) + self.actionPipe.writeln(action_string) # sends the actions for the game to perform before the next step + return data + + # Testing + # Safely stops all of the processes related to running the emulator + def close(self): + self.console.close() + self.actionPipe.close() + self.dataPipe.close() diff --git a/src/emulator/StreamGobbler.py b/src/emulator/StreamGobbler.py new file mode 100755 index 0000000..9952c61 --- /dev/null +++ b/src/emulator/StreamGobbler.py @@ -0,0 +1,32 @@ +import threading + + +# A thread used for reading data from a thread +# pipes don't have very good time out functionality, so this is used in combination with a queue +class StreamGobbler(threading.Thread): + + def __init__(self, pipe, queue, debug=False): + threading.Thread.__init__(self) + self.pipe = pipe + self.queue = queue + self.debug = debug + self._stop_event = threading.Event() + self.has_cursor = False + + def run(self): + for line in iter(self.pipe.readline, b''): + if self.debug: + print(line) + self.queue.put(line[:-1]) + if self._stop_event.is_set(): + break + + def wait_for_cursor(self): + new_line_count = 0 + while new_line_count != 3: + line = self.pipe.readline() + if line == b'\n': + new_line_count += 1 + + def stop(self): + self._stop_event.set() diff --git a/src/emulator/mame/cfg/sfiii3n.cfg b/src/emulator/mame/cfg/sfiii3n.cfg new file mode 100644 index 0000000..b01f5f7 --- /dev/null +++ b/src/emulator/mame/cfg/sfiii3n.cfg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/emulator/mame/history/history.db b/src/emulator/mame/history/history.db new file mode 100644 index 0000000000000000000000000000000000000000..3f40763abdab068067d45a96c4d9fe8ca29e379d GIT binary patch literal 12288 zcmeI#%SyvQ6oBEGC|(LB-F1=8EEQA`A3(^kD`mNx-8AAZYniY^Z0R*hkq}fUwTJV9Y|5H zm(}O?t+8EwQNEkD%ZvW(;=13szW&5{j@>kI6M+B%2q1s}0tg_000IagfB*vfDll>v eqyOL6-^;xaKmY**5I_I{1Q0*~0R#}}1bzXr8#}!K literal 0 HcmV?d00001 diff --git a/src/emulator/mame/plugins/README.md b/src/emulator/mame/plugins/README.md new file mode 100755 index 0000000..de97ed8 --- /dev/null +++ b/src/emulator/mame/plugins/README.md @@ -0,0 +1,3 @@ +# **Plugins** # + +LUA plugins contains code from various sources so license is per file. diff --git a/src/emulator/mame/plugins/boot.lua b/src/emulator/mame/plugins/boot.lua new file mode 100755 index 0000000..5ed1ac8 --- /dev/null +++ b/src/emulator/mame/plugins/boot.lua @@ -0,0 +1,52 @@ +-- license:BSD-3-Clause +-- copyright-holders:Miodrag Milanovic +require('lfs') + +-- add helper to lfs for plugins to use +function lfs.env_replace(str) + local pathsep = package.config:sub(1,1) + local function dorep(val) + ret = os.getenv(val) + if ret then + return ret + end + return val + end + + if pathsep == '\\' then + str = str:gsub("%%(%w+)%%", dorep) + else + str = str:gsub("%$(%w+)", dorep) + end + return str +end + +_G._ = emu.lang_translate +local dir = lfs.env_replace(manager:options().entries.pluginspath:value()) + +package.path = dir .. "/?.lua;" .. dir .. "/?/init.lua" + +local json = require('json') +local function readAll(file) + local f = io.open(file, "rb") + local content = f:read("*all") + f:close() + return content +end + +for file in lfs.dir(dir) do + if (file~="." and file~=".." and lfs.attributes(dir .. "/" .. file,"mode")=="directory") then + local filename = dir .. "/" .. file .. "/plugin.json" + local meta = json.parse(readAll(filename)) + if (meta["plugin"]["type"]=="plugin") and (mame_manager:plugins().entries[meta["plugin"]["name"]]~=nil) then + local entry = mame_manager:plugins().entries[meta["plugin"]["name"]] + if (entry:value()==true) then + emu.print_verbose("Starting plugin " .. meta["plugin"]["name"] .. "...") + plugin = require(meta["plugin"]["name"]) + if plugin.set_folder~=nil then plugin.set_folder(dir .. "/" .. file) end + plugin.startplugin(); + end + end + end +end + diff --git a/src/emulator/mame/plugins/cheat/cheat_json.lua b/src/emulator/mame/plugins/cheat/cheat_json.lua new file mode 100755 index 0000000..fd2778b --- /dev/null +++ b/src/emulator/mame/plugins/cheat/cheat_json.lua @@ -0,0 +1,12 @@ +local jsoncheat = {} + +function jsoncheat.filename(name) + return name .. ".json" +end + +function jsoncheat.conv_cheat(data) + local json = require("json") + return json.parse(data) +end + +return jsoncheat diff --git a/src/emulator/mame/plugins/cheat/cheat_simple.lua b/src/emulator/mame/plugins/cheat/cheat_simple.lua new file mode 100755 index 0000000..11c63e6 --- /dev/null +++ b/src/emulator/mame/plugins/cheat/cheat_simple.lua @@ -0,0 +1,321 @@ +-- converter for simple cheats +-- simple cheats are single/linked address every frame ram, rom or gg,ar cheats in one file called cheat.simple +-- +-- ram/rom cheat format: ,,,,, +-- only program address space is supported, comments are prepended with ; +-- size is b - u8, w - u16, d - u32, q - u64 +-- +-- gg,ar cheat format: ,,, like "nes/smb,gg,SXIOPO,Infinite Lives" +-- gg for game genie -- nes, snes, megadriv, gamegear, gameboy +-- ar for action replay -- nes, snes, megadriv, gamegear, sms +-- +-- use "^" as description to link to previous cheat +-- set name is / like "nes/smb" for softlist items +-- Don't use commas in the description + +local simple = {} + +simple.romset = "????" + +function simple.filename(name) + simple.romset = name + return "cheat.simple" +end + +local codefuncs = {} +local currcheat + +local function prepare_rom_cheat(desc, region, addr, val, size, banksize, comp) + local cheat + if desc:sub(1,1) ~= "^" then + currcheat = { desc = desc, region = { rom = region } } + currcheat.script = { off = string.format([[ + if on then + for k, v in pairs(addrs) do + rom:write_u%d(v.addr, v.save) + end + end]], size), + on = string.format([[ + addrs = { + --flag + } + on = true + for k, v in pairs(addrs) do + v.save = rom:read_u%d(v.addr) + rom:write_u%d(v.addr, v.val) + end]], size, size) } + cheat = currcheat + + end + if banksize and comp then + local rom = manager:machine():memory().regions[region] + local bankaddr = addr & (banksize - 1) + addr = nil + if not rom then + error("rom cheat invalid region " .. desc) + end + for i = 0, rom.size, banksize do + if rom:read_u8(i + bankaddr) == comp then + addr = i + bankaddr + break + end + end + if not addr then + error("rom cheat compare value not found " .. desc) + end + end + currcheat.script.on = currcheat.script.on:gsub("%-%-flag", string.format("{addr = %d, val = %d},\n--flag", addr, val), 1) + return cheat +end + +local function prepare_ram_cheat(desc, tag, addr, val, size) + local cheat + if desc:sub(1,1) ~= "^" then + currcheat = { desc = desc, space = { cpup = { tag = tag, type = "program" } }, script = { run = "" } } + cheat = currcheat + end + currcheat.script.run = currcheat.script.run .. " cpup:write_u" .. size .. "(" .. addr .. "," .. val .. ", true)" + return cheat +end + +function codefuncs.nes_gg(desc, code) + local xlate = { A = 0, P = 1, Z = 2, L = 3, G = 4, I = 5, T = 6, Y = 7, E = 8, + O = 9, X = 10, U = 11, K = 12, S = 13, V = 14, N = 15 } + local value = 0 + code:upper():gsub("(.)", function(s) + if not xlate[s] then + error("error parsing game genie cheat " .. desc) + end + value = (value << 4) | xlate[s] + end) + local addr, newval, comp + if #code == 6 then + addr = ((value >> 4) & 7) | ((value >> 8) & 0x78) | ((value >> 12) & 0x80) | ((value << 8) & 0x700) | ((value << 4) & 0x7800) + newval = ((value >> 20) & 7) | (value & 8) | ((value >> 12) & 0x70) | ((value >> 16) & 0x80) + return prepare_rom_cheat(desc, ":nes_slot:cart:prg_rom", addr, newval, 8) + elseif #code == 8 then + addr = ((value >> 12) & 7) | ((value >> 16) & 0x78) | ((value >> 20) & 0x80) | (value & 0x700) | ((value >> 4) & 0x7800) + newval = ((value >> 28) & 7) | (value & 8) | ((value >> 20) & 0x70) | ((value >> 24) & 0x80) + comp = ((value >> 4) & 7) | ((value >> 8) & 8) | ((value << 4) & 0x70) | (value & 0x80) + -- assume 8K banks, 32K also common but is an easy multiple of 8K + return prepare_rom_cheat(desc, ":nes_slot:cart:prg_rom", addr, newval, 8, 8192, comp) + else + error("error game genie cheat incorrect length " .. desc) + end +end + +function codefuncs.nes_ar(desc, code) + code = code:gsub("[: %-]", "") + if #code ~= 8 then + error("error action replay cheat incorrect length " .. desc) + end + local newval = tonumber(code:sub(7, 8), 16) + local addr = tonumber(code:sub(3, 6), 16) + if not newval or not addr then + error("error parsing action replay cheat " .. desc) + end + return prepare_ram_cheat(desc, ":maincpu", addr, newval, 8) +end + +local function snes_prepare_cheat(desc, addr, val) + local bank = addr >> 16 + local offset = addr & 0xffff + if ((bank <= 0x3f) and (offset < 0x2000)) or ((bank & 0xfe) == 0x7e) then + return prepare_ram_cheat(desc, ":maincpu", addr, val, 8) + end + if (manager:machine().devices[":maincpu"].spaces["program"]:read_u8(0xffd5) & 1) == 1 then --hirom + if (bank & 0x7f) <= 0x3f and offset >= 0x8000 then + -- direct map + elseif (bank & 0x7f) >= 0x40 and (bank & 0x7f) <= 0x7d then + addr = addr & 0x3fffff + elseif bank >= 0xfe then + addr = addr & 0x3fffff + else + error("error cheat not rom or ram addr " .. desc) + end + else --lorom + if (bank & 0x7f) <= 0x3f and offset >= 0x8000 then + addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff) + elseif (bank & 0x7f) >= 0x40 and (bank & 0x7f) <= 0x6f then + addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff) + elseif (bank & 0x7f) >= 0x70 and (bank & 0x7f) <= 0x7d and offset >= 0x8000 then + addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff) + elseif bank >= 0xfe and offset >= 0x8000 then + addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff) + else + error("error cheat not rom or ram addr " .. desc) + end + end + return prepare_rom_cheat(desc, ":snsslot:cart:rom", addr, val, 8) +end + +function codefuncs.snes_gg(desc, code) + local xlate = { D = 0, F = 1, ["4"] = 2, ["7"] = 3, ["0"] = 4, ["9"] = 5, ["1"] = 6, ["5"] = 7, + ["6"] = 8, B = 9, C = 10, ["8"] = 11, A = 12, ["2"] = 13, ["3"] = 14, E = 15 } + local value = 0 + local count = 0 + code:upper():gsub("(.)", function(s) + if s == "-" then + return + elseif not xlate[s] then + error("error parsing game genie cheat " .. desc) + end + count = count + 1 + value = (value << 4) | xlate[s] + end) + if count ~= 8 then + error("error game genie cheat incorrect length " .. desc) + end + local newval = (value >> 24) & 0xff + local addr = ((value >> 6) & 0xf) | ((value >> 12) & 0xf0) | ((value >> 6) & 0x300) | ((value << 10) & 0xc00) | + ((value >> 8) & 0xf000) | ((value << 14) & 0xf0000) | ((value << 10) & 0xf00000) + return snes_prepare_cheat(desc, addr, newval) +end + +function codefuncs.snes_ar(desc, code) + code = code:gsub("[: %-]", "") + if #code ~= 8 then + error("error action replay cheat incorrect length " .. desc) + end + local addr = tonumber(code:sub(1, 6), 16) + local val = tonumber(code:sub(7, 8), 16) + if not addr or not val then + error("error parsing action replay cheat " .. desc) + end + return snes_prepare_cheat(desc, addr, val) +end + +function codefuncs.megadriv_gg(desc, code) + local xlate = { A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, J = 8, K = 9, L = 10, M = 11, N = 12, + P = 13, R = 14, S = 15, T = 16, V = 17, W = 18, X = 19, Y = 20, Z = 21, ["0"] = 22, ["1"] = 23, + ["2"] = 24, ["3"] = 25, ["4"] = 26, ["5"] = 27, ["6"] = 28, ["7"] = 29, ["8"] = 30, ["9"] = 31 } + local value = 0 + local count = 0 + code:upper():gsub("(.)", function(s) + if s == "-" then + return + elseif not xlate[s] then + error("error parsing game genie cheat " .. desc) + end + count = count + 1 + value = (value << 5) | xlate[s] + end) + if count ~= 8 then + error("error game genie cheat incorrect length " .. desc) + end + local newval = ((value >> 32) & 0xff) | ((value >> 3) & 0x1f00) | ((value << 5) & 0xe000) + local addr = (value & 0xff00ff) | ((value >> 16) & 0xff00) + return prepare_rom_cheat(desc, ":mdslot:cart:rom", addr, newval, 16) +end + +function codefuncs.megadriv_ar(desc, code) + code = code:gsub("[: %-]", "") + if #code ~= 10 then + error("error action replay cheat incorrect length " .. desc) + end + local addr = tonumber(code:sub(1, 6), 16) + local val = tonumber(code:sub(7, 10), 16) + if addr < 0xff0000 then + error("error action replay cheat not ram addr " .. desc) + end + return prepare_ram_cheat(desc, ":maincpu", addr, val, 16) +end + +local function gbgg_ggcodes(desc, code, region) + code = code:gsub("%-", "") + local comp + if #code == 6 then + comp = -1 + elseif #code == 9 then + comp = ~tonumber(code:sub(7, 7) .. code:sub(9, 9), 16) & 0xff + comp = ((comp >> 2) | ((comp << 6) & 0xc0)) ~ 0x45 + else + error("error game genie cheat incorrect length " .. desc) + end + local newval = tonumber(code:sub(1, 2), 16) + local addr = tonumber(code:sub(6, 6) .. code:sub(3, 5), 16) + if not newval or not addr or not comp then + error("error parsing game genie cheat " .. desc) + end + addr = (~addr & 0xf000) | (addr & 0xfff) + if addr > 0x7fff then + error("error game genie cheat bad addr " .. desc) + end + if comp == -1 then + return prepare_rom_cheat(desc, region, addr, newval, 8) + else + -- assume 8K banks + return prepare_rom_cheat(desc, region, addr, newval, 8, 8192, comp) + end + return cheat +end + +function codefuncs.gameboy_gg(desc, code) + return gbgg_ggcodes(desc, code, ":gbslot:cart:rom") +end + +function codefuncs.gamegear_gg(desc, code) + return gbgg_ggcodes(desc, code, ":slot:cart:rom") +end + +function codefuncs.gamegear_ar(desc, code) + code = code:gsub("[: %-]", "") + if #code ~= 8 then + error("error action replay cheat incorrect length " .. desc) + end + local addr = tonumber(code:sub(1, 6), 16) + local val = tonumber(code:sub(7, 8), 16) + if addr < 0xc000 or addr >= 0xe000 then + error("error action replay cheat not ram addr " .. desc) + end + return prepare_ram_cheat(desc, ":maincpu", addr, val, 8) +end + +codefuncs.sms_ar = codefuncs.gamegear_ar + +function simple.conv_cheat(data) + local cheats = {} + for line in data:gmatch('([^\n;]+)') do + local set, cputag, offset, size, val, desc = line:match('([^,]+),([^,]+),([^,]+),?([^,]*),?([^,]*),(.*)') + if set == simple.romset then + local cheat + if cputag:sub(1,1) ~= ":" then + local list, name = set:match('([^/]+)/(.+)') + local func = list .. "_" .. cputag + if list and desc and codefuncs[func] then + local status + status, cheat = pcall(codefuncs[func], desc, offset) + if not status then + emu.print_error(cheat) + cheat = nil + end + end + elseif size and val then + if size == "w" then + size = 16 + elseif size == "d" then + size = 32 + elseif size == "q" then + size = 64 + else + size = 8 + end + offset = tonumber(offset, 16) + val = tonumber(val, 16) + if manager:machine().devices[cputag] then + cheat = prepare_ram_cheat(desc, cputag, offset, val, size) + else + cheat = prepare_rom_cheat(desc, cputag, offset, val, size) + end + end + if cheat then + cheats[#cheats + 1] = cheat + end + end + end + currcheat = nil + return cheats +end + +return simple + diff --git a/src/emulator/mame/plugins/cheat/cheat_xml.lua b/src/emulator/mame/plugins/cheat/cheat_xml.lua new file mode 100755 index 0000000..a14aee0 --- /dev/null +++ b/src/emulator/mame/plugins/cheat/cheat_xml.lua @@ -0,0 +1,251 @@ +local xml = {} + +function xml.filename(name) + return name .. ".xml" +end + +-- basic xml parser for mamecheat only +local function xml_parse(data) + local function fix_gt(str) + str = str:gsub(">=", " ge ") + str = str:gsub(">", " gt ") + return str + end + data = data:gsub("(condition=%b\"\")", fix_gt) + local cheat_str = data:match("(.*)") + + local function get_tags(str) + local arr = {} + while str ~= "" do + local tag, attr, stop + tag, attr, stop, str = str:match("<([%w!%-]+) ?(.-)(/?)[ %-]->(.*)") + + if not tag then + return arr + end + if tag:sub(0, 3) ~= "!--" then + local block = {} + if stop ~= "/" then + local nest + nest, str = str:match("(.-)(.*)") + local children = get_tags(nest) + if not next(children) then + nest = nest:gsub(""] = 90, -- Arrow + ["==>"] = 91, -- Continue Arrow + ["hcb"] = 100, -- Half Circle Back + ["huf"] = 101, -- Half Circle Front Up + ["hcf"] = 102, -- Half Circle Front + ["hub"] = 103, -- Half Circle Back Up + ["qfd"] = 104, -- 1/4 Cir For 2 Down + ["qdb"] = 105, -- 1/4 Cir Down 2 Back + ["qbu"] = 106, -- 1/4 Cir Back 2 Up + ["quf"] = 107, -- 1/4 Cir Up 2 For + ["qbd"] = 108, -- 1/4 Cir Back 2 Down + ["qdf"] = 109, -- 1/4 Cir Down 2 For + ["qfu"] = 110, -- 1/4 Cir For 2 Up + ["qub"] = 111, -- 1/4 Cir Up 2 Back + ["fdf"] = 112, -- Full Clock Forward + ["fub"] = 113, -- Full Clock Back + ["fuf"] = 114, -- Full Count Forward + ["fdb"] = 115, -- Full Count Back + ["xff"] = 116, -- 2x Forward + ["xbb"] = 117, -- 2x Back + ["dsf"] = 118, -- Dragon Screw Forward + ["dsb"] = 119, -- Dragon Screw Back + -- Big letter Text + ["AIR"] = 121, -- AIR + ["DIR"] = 122, -- DIR + ["MAX"] = 123, -- MAX + ["TAP"] = 124, -- TAP + -- Condition of Positions + ["jump"] = 125, -- Jump + ["hold"] = 126, -- Hold + ["air"] = 127, -- Air + ["sit"] = 128, -- Squatting + ["close"] = 129, -- Close + ["away"] = 130, -- Away + ["charge"] = 131, -- Charge + ["tap"] = 132, -- Serious Tap + ["button"] = 133, -- Any Button +} + +local function convert_char(str) + str = str:gsub("@([a-zA-Z%-]+)", function(s) if convert_text[s] then return utf8.char(convert_text[s] + 0xe000) end return s end) + str = str:gsub("_(.)", function(s) if default_text[s] then return utf8.char(default_text[s] + 0xe000) end return s end) + str = str:gsub("%^(.)", function(s) if expand_text[s] then return utf8.char(expand_text[s] + 0xe000) end return s end) + return str +end + +return convert_char diff --git a/src/emulator/mame/plugins/data/data_command.lua b/src/emulator/mame/plugins/data/data_command.lua new file mode 100755 index 0000000..e85533d --- /dev/null +++ b/src/emulator/mame/plugins/data/data_command.lua @@ -0,0 +1,30 @@ +local dat = {} +local info, ver +local datread = require("data/load_dat") +do + local convert = require("data/button_char") + datread, ver = datread.open("command.dat", "# Command List%-.+hand", convert) +end + +function dat.check(set, softlist) + if softlist or not datread then + return nil + end + local status + status, info = pcall(datread, "cmd", "info", set) + if not status or not info then + return nil + end + info = "#jf\n" .. info + return _("Command") +end + +function dat.get() + return info +end + +function dat.ver() + return ver +end + +return dat diff --git a/src/emulator/mame/plugins/data/data_gameinit.lua b/src/emulator/mame/plugins/data/data_gameinit.lua new file mode 100755 index 0000000..7b93334 --- /dev/null +++ b/src/emulator/mame/plugins/data/data_gameinit.lua @@ -0,0 +1,27 @@ +local dat = {} +local ver, info + +local datread = require("data/load_dat") +datread, ver = datread.open("gameinit.dat", "# .-GAMEINIT.DAT") + +function dat.check(set, softlist) + if softlist or not datread then + return nil + end + local status + status, info = pcall(datread, "mame", "info", set) + if not status or not info then + return nil + end + return _("Gameinit") +end + +function dat.get() + return info +end + +function dat.ver() + return ver +end + +return dat diff --git a/src/emulator/mame/plugins/data/data_hiscore.lua b/src/emulator/mame/plugins/data/data_hiscore.lua new file mode 100755 index 0000000..9c888dd --- /dev/null +++ b/src/emulator/mame/plugins/data/data_hiscore.lua @@ -0,0 +1,828 @@ +-- to use this get the package from http://greatstone.free.fr/hi2txt/ +-- extract the hi2txt.zip and place it in your history path + +local dat = {} +local env = {} +local output +local curset + +function env.open(file, size) + if file == ".hi" then + local path = "hi" + local ini = emu.file(lfs.env_replace(manager:options().entries.inipath:value()), 1) + local ret = ini:open("hiscore.ini") + if not ret then + local inifile = ini:read(ini:size()) + for line in inifile:gmatch("[^\n\r]") do + token, value = string.match(line, '([^ ]+) ([^ ]+)'); + if token == "hi_path" then + path = value + break + end + end + end + file = path .. "/" .. curset .. ".hi" + else + file = lfs.env_replace(manager:options().entries.nvram_directory:value()) .. "/" .. curset .. "/" .. file + end + local f = io.open(file, "rb") + local content = f:read("*all") + f:close() + if #content < size then + content = content .. string.rep("\0", size - #content) + end + return content +end + +function env.endianness(bytes, endian) + local newbytes = {} + if endian == "little_endian" then + for i = 1, #bytes do + newbytes[i] = bytes[#bytes - i + 1] + end + else + newbytes = bytes + end + return newbytes +end + +function env.byte_skip(bytes, skip) + local newbytes = {} + if skip == "odd" then + -- lua lists are 1 based so use even indexes + for i = 2, #bytes, 2 do + newbytes[i/2] = bytes[i] + end + elseif skip == "even" then + for i = 1, #bytes, 2 do + newbytes[(i+1)/2] = bytes[i] + end + elseif skip == "1000" then + for i = 1, #bytes, 4 do + newbytes[(i+3)/4] = bytes[i] + end + elseif skip == "0100" then + for i = 2, #bytes, 4 do + newbytes[(i+2)/4] = bytes[i] + end + elseif skip == "0010" then + for i = 3, #bytes, 4 do + newbytes[(i+1)/4] = bytes[i] + end + elseif skip == "0001" then + for i = 4, #bytes, 4 do + newbytes[i/4] = bytes[i] + end + else + skip = tonumber(skip) + for i = 1, #bytes do + if bytes[i] ~= skip then + newbytes[#newbytes + 1] = bytes[i] + end + end + end + return newbytes +end + +function env.byte_trim(bytes, val) + val = tonumber(val) + for i = 1, #bytes do + if bytes[i] ~= val then + return bytes + end + table.remove(bytes, 1) + end + return bytes +end + +function env.byte_swap(bytes, val) + local newbytes = {} + val = tonumber(val) + for i = 1, #bytes do + local off = i + val - 1 - 2 * ((i - 1) % val) + if off > #bytes then -- ?? + break + end + newbytes[i] = bytes[off] + end + return newbytes +end + +function env.nibble_skip(bytes, skip) + local newbytes = {} + if skip == "odd" then + for i = 1, #bytes, 2 do + val1 = bytes[i]:byte(1) + val2 = bytes[i+1]:byte(1) + newbytes[(i+1)/2] = string.char(((val1 & 0x0f) << 4) | (val2 & 0x0f)) + end + elseif skip == "even" then + for i = 1, #bytes, 2 do + val1 = bytes[i]:byte(1) + val2 = bytes[i+1]:byte(1) + newbytes[(i+1)/2] = string.char((val1 & 0xf0) | ((val2 & 0xf0) >> 4)) + end + end + return newbytes +end + +function env.bit_swap(bytes, swap) + if swap == "yes" then + for i = 1, #bytes do + val = bytes[i]:byte(1) + bytes[i] = string.char(((val & 1) << 7) | ((val & 2) << 5) | ((val & 4) << 3) | ((val & 8) << 1) | ((val & 0x10) >> 1) | ((val & 0x20) >> 3) | ((val & 0x40) >> 5) | ((val & 0x80) >> 7)) + end + end + return bytes +end + +function env.bitmask(bytes, mask) + local newbytes = 0 + bytes = string.unpack(">I" .. #bytes, table.concat(bytes)) + for i = 1, #mask do + newbytes = newbytes | (((bytes >> mask.ishift) & mask.mask) << mask.oshift) + end + bytes = {} + while newbytes ~= 0 do + bytes[#bytes + 1] = newbytes & 0xff + newbytes = newbytes >> 8 + end + newbytes = {} + for i = 1, #bytes do + newbytes[i] = string.char(bytes[#bytes + 1 - i]) + end + return newbytes +end + +function env.frombcd(val) + local result = 0 + local mul = 1 + while val ~= 0 do + result = result + ((val % 16) * mul) + val = val >> 4 + mul = mul * 10 + end + return result +end + +function env.basechar(bytes, base) + emu.print_verbose("data_hiscore: basechar " .. base .. " unimplemented\n") + if base == "32" then + elseif base == "40" then + end + return bytes +end + +function env.charset_conv(bytes, charset) + if type(charset) == "string" then + local chartype, offset, delta = charset:match("CS_(%w*)%[?(%-?%d?%d?),?(%d?%d?)%]?") + if chartype == "NUMBER" then + + end + emu.print_verbose("data_hiscore: charset " .. chartype .. " unimplemented\n") + return bytes + end + for num, char in ipairs(bytes) do + char = string.byte(char) + if charset[char] then + bytes[num] = charset[char] + elseif charset.default then + bytes[num] = charset.default + end + end + return bytes +end + +function env.ascii_step(bytes, step) + for num, char in ipairs(bytes) do + bytes[num] = string.char(char:byte() / step) + end + return bytes +end + +function env.ascii_offset(bytes, offset) + for num, char in ipairs(bytes) do + bytes[num] = string.char(char:byte() + offset) + end + return bytes +end + +env.tostring = tostring +env.type = type +env.table = { pack = table.pack, concat = table.concat } +env.string = { unpack = string.unpack, format = string.format, rep = string.rep, gsub = string.gsub, lower = string.lower, upper = string.upper } +env.math = { min = math.min, max = math.max, floor = math.floor } + +do + local function readonly(t) + local mt = { __index = t, __newindex = function(t, k, v) return end } + return setmetatable({}, mt) + end + env.table = readonly(env.table) + env.string = readonly(env.string) + env.math = readonly(env.math) + env = readonly(env) +end + +function dat.check(set, softlist) + if softlist then + return nil + end + local datpath + local function xml_parse(file) + local table + datpath = file:fullpath():gsub(".zip", "/") + local data = file:read(file:size()) + data = data:match("(.*)") + local function get_tags(str, parent) + local arr = {} + while str ~= "" do + local tag, attr, stop + tag, attr, stop, str = str:match("<([%w!_%-]+) ?(.-)(/?)[ %-]->(.*)") + + if not tag then + return arr + end + if tag:sub(0, 3) ~= "!--" then + local block = {} + if stop ~= "/" then + local nest + nest, str = str:match("(.-)(.*)") + local children = get_tags(nest, tag) + if not next(children) then + nest = nest:gsub("