Skip to content

Commit

Permalink
Make sure that all inputs are tracked by HTTP event stream (#397)
Browse files Browse the repository at this point in the history
Adds a buffer for input events so that each time the HTTP server sends an `Inputs` event via the event stream (every 1/30th of a second) it is guaranteed to contain _all_ buttons that have been pressed since the last update.

That means that the event does not necessarily report the exact state of the buttons but more of an 'echo'.

For example, when using the Spin mode unthrottled, the event will likely report all 4 directional buttons to have been pressed at the same time.

But I think that's a good tradeoff between accuracy and performance.

I've also changed the previously introduced mechanism to wait for the `work_queue` by using an `Event` to using the built-in `task_done()` and `join()` methods of the queue object. Just found those by accident in the Python docs.
  • Loading branch information
hanzi authored Sep 12, 2024
1 parent 8541075 commit 20d3dcd
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 22 deletions.
19 changes: 8 additions & 11 deletions modules/libmgba.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
}


def inputs_to_strings(inputs: int) -> list[str]:
"""
:return: Converts the bitfield representing the emulator's input state to a list
of button names that are being pressed.
"""
return [key for key in input_map if inputs & input_map[key]]


class PerformanceTracker:
"""
This is a little helper utility used for measuring the FPS rate and allowing
Expand Down Expand Up @@ -429,17 +437,6 @@ def get_inputs(self) -> int:
"""
return self._core._core.getKeys(self._core._core)

def get_inputs_as_strings(self) -> list[str]:
"""
:return: A list of all the button names that are currently being pressed
"""
raw_inputs = self.get_inputs()
inputs = []
for key in input_map:
if raw_inputs & input_map[key]:
inputs.append(key)
return inputs

def set_inputs(self, inputs: int):
"""
:param inputs: A bitfield with all the buttons that should now be pressed
Expand Down
8 changes: 8 additions & 0 deletions modules/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import queue
import sys
from collections import deque
from threading import Thread

from modules.console import console
Expand All @@ -16,6 +17,11 @@
work_queue: queue.Queue[callable] = queue.Queue()


# Keeps a list of inputs that have been pressed for each frame so that the HTTP server
# can fetch and accumulate them for its `Inputs` stream event.
inputs_each_frame: deque[int] = deque(maxlen=128)


def main_loop() -> None:
"""
This function is run after the user has selected a profile and the emulator has been started.
Expand Down Expand Up @@ -43,6 +49,7 @@ def main_loop() -> None:
while not work_queue.empty():
callback = work_queue.get_nowait()
callback()
work_queue.task_done()

context.frame += 1

Expand Down Expand Up @@ -109,6 +116,7 @@ def main_loop() -> None:
else:
context.set_manual_mode()

inputs_each_frame.append(context.emulator.get_inputs())
context.emulator.run_single_frame()
previous_frame_info = frame_info
previous_frame_info.previous_frame = None
Expand Down
10 changes: 3 additions & 7 deletions modules/web/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import json
import time
from pathlib import Path
from threading import Event

import waitress
from apispec import APISpec
Expand All @@ -17,6 +16,7 @@
from modules.files import read_file
from modules.game import _event_flags
from modules.items import get_item_bag, get_item_storage
from modules.libmgba import inputs_to_strings
from modules.main import work_queue
from modules.map import get_map_data
from modules.map_data import MapFRLG, MapRSE
Expand Down Expand Up @@ -65,19 +65,15 @@ def _update_via_work_queue(
if state_cache_entry.age_in_frames < maximum_age_in_frames:
return

update_event = Event()

def do_update():
try:
update_callback()
except Exception:
console.print_exception()
finally:
update_event.set()

try:
work_queue.put_nowait(do_update)
update_event.wait(timeout=5)
work_queue.join()
except Exception:
console.print_exception()
return
Expand Down Expand Up @@ -617,7 +613,7 @@ def http_get_input():
tags:
- emulator
"""
return jsonify(context.emulator.get_inputs_as_strings())
return jsonify(inputs_to_strings(context.emulator.get_inputs()))

@server.route("/input", methods=["POST"])
def http_post_input():
Expand Down
14 changes: 10 additions & 4 deletions modules/web/http_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

from modules.console import console
from modules.context import context
from modules.main import work_queue
from modules.libmgba import inputs_to_strings
from modules.main import work_queue, inputs_each_frame
from modules.memory import GameState, get_game_state
from modules.player import get_player, get_player_avatar
from modules.pokedex import get_pokedex
Expand Down Expand Up @@ -202,9 +203,14 @@ def run_watcher():
previous_emulator_state["message"] = context.message
send_message(DataSubscription.Message, data=context.message, event_type="Message")

if subscriptions["Inputs"] > 0 and context.emulator.get_inputs() != previous_emulator_state["inputs"]:
previous_emulator_state["inputs"] = context.emulator.get_inputs()
send_message(DataSubscription.Inputs, data=context.emulator.get_inputs_as_strings(), event_type="Inputs")
if subscriptions["Inputs"] > 0:
combined_inputs = 0
for _ in range(len(inputs_each_frame)):
combined_inputs |= inputs_each_frame.popleft()

if combined_inputs != previous_emulator_state["inputs"]:
previous_emulator_state["inputs"] = combined_inputs
send_message(DataSubscription.Inputs, data=inputs_to_strings(combined_inputs), event_type="Inputs")

if subscriptions["EmulatorSettings"] > 0:
if context.emulation_speed != previous_emulator_state["emulation_speed"]:
Expand Down

0 comments on commit 20d3dcd

Please sign in to comment.