272 lines
12 KiB
Python
Executable File
272 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Launch PGFW games in the `games/` folder
|
|
|
|
import argparse, os, pathlib, pygame, sys
|
|
import lib.pgfw.pgfw as pgfw
|
|
from games.ibitfit.electric_sieve.ElectricSieve import ElectricSieve
|
|
|
|
# Import GPIO library if available
|
|
try:
|
|
import RPi.GPIO as GPIO
|
|
except ImportError:
|
|
pass
|
|
|
|
class Playzing(pgfw.Game):
|
|
|
|
# The GPIO pins corresponding to the buttons
|
|
PIN_BUTTON_LEFT = 24
|
|
PIN_BUTTON_RIGHT = 23
|
|
|
|
def __init__(self, config_overrides=None):
|
|
"""
|
|
Create logo sprite, clear screen, and subscribe to events.
|
|
"""
|
|
pgfw.Game.__init__(self)
|
|
|
|
# Member dict for tracking pin state changes. Start at 1 because high means unpressed.
|
|
self.pin_states = {
|
|
self.PIN_BUTTON_LEFT: 1,
|
|
self.PIN_BUTTON_RIGHT: 1
|
|
}
|
|
|
|
# Assign types to configuration values
|
|
self.configuration.type_declarations.add_chart({
|
|
"display":
|
|
{
|
|
"int-list": "clear",
|
|
"bool": "rotated"
|
|
},
|
|
"logo":
|
|
{
|
|
"path": ["fire", "text"],
|
|
"int": ["margin", "restart", "delay"],
|
|
"bool": "trail"
|
|
}
|
|
})
|
|
|
|
# Merge config overrides from the command line
|
|
self.configuration.merge_command_line()
|
|
|
|
# Clear screen to black
|
|
self.get_display_surface().fill(self.configuration.get("display", "clear"))
|
|
|
|
# Initialize GPIO input and callbacks if GPIO library is loaded
|
|
if "RPi.GPIO" in sys.modules:
|
|
self.initialize_gpio()
|
|
|
|
# Subscribe to PGFW command events
|
|
self.subscribe(self.respond)
|
|
|
|
# Load sprites for logo parts
|
|
self.logo_text = pgfw.Sprite(self)
|
|
self.logo_text.load_from_path(self.get_resource(self.configuration.get("logo", "text")), True)
|
|
if self.rotated:
|
|
self.logo_text.rotate()
|
|
self.logo_fire = pgfw.Sprite(self)
|
|
self.logo_fire.load_from_path(self.get_resource(self.configuration.get("logo", "fire")), True)
|
|
if self.rotated:
|
|
self.logo_fire.rotate()
|
|
|
|
# Register logo animation functions and begin animation
|
|
self.register(self.animate_logo)
|
|
self.register(self.reveal_text, interval=40)
|
|
self.play(self.animate_logo, play_once=True)
|
|
|
|
# Don't let modifier keys trigger an any key event
|
|
self.input.suppress_any_key_on_mods()
|
|
|
|
def initialize_gpio(self):
|
|
"""
|
|
Set pin numbering mode to GPIO, initialize all buttons to input pullup.
|
|
"""
|
|
# Use GPIO numbering
|
|
GPIO.setmode(GPIO.BCM)
|
|
|
|
# Set button pins to pullup and attach to each a callback that runs on press or release
|
|
for pin in self.PIN_BUTTON_LEFT, self.PIN_BUTTON_RIGHT:
|
|
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
|
GPIO.add_event_detect(pin, GPIO.BOTH, self.gpio_input)
|
|
|
|
def gpio_input(self, pin):
|
|
"""
|
|
Translate GPIO input into PGFW commands, so Raspberry Pi wired controllers be used for input.
|
|
|
|
Compare the pin state to what is stored in memory. Only fire an event if there has been a change in state. A change
|
|
from high to low triggers a press event. A change from low to high triggers a press cancel event.
|
|
|
|
@param pin Raspberry Pi pin number as read by the RPi.GPIO library
|
|
"""
|
|
# Print the input state of each pin if debug is requested on the command line
|
|
if "--debug" in sys.argv:
|
|
pin_name = "left" if pin == self.PIN_BUTTON_LEFT else "right"
|
|
left_pin_state = GPIO.input(self.PIN_BUTTON_LEFT)
|
|
right_pin_state = GPIO.input(self.PIN_BUTTON_RIGHT)
|
|
print(f"Received {pin} ({pin_name}) input. Left state is {left_pin_state}. Right state is {right_pin_state}")
|
|
|
|
# If the saved state of the pin is the same, there hasn't been a real button press or release, so don't continue
|
|
if self.pin_states[pin] != GPIO.input(pin):
|
|
self.pin_states[pin] = GPIO.input(pin)
|
|
# A high signal means the button is released, and a low signal means it is pressed
|
|
cancel = not (GPIO.input(pin) == GPIO.LOW)
|
|
if pin == self.PIN_BUTTON_LEFT:
|
|
self.input.post_command("left", cancel=cancel)
|
|
elif pin == self.PIN_BUTTON_RIGHT:
|
|
self.input.post_command("right", cancel=cancel)
|
|
self.input.post_any_command(id=pin, cancel=cancel)
|
|
|
|
@property
|
|
def rotated(self):
|
|
return self.configuration.get("display", "rotated")
|
|
|
|
def orient(self, obj):
|
|
oriented = obj
|
|
if self.rotated:
|
|
if isinstance(obj, pygame.Rect):
|
|
oriented = pygame.Rect(obj.y, self.get_display_surface().get_height() - obj.x + obj.w, obj.h, obj.w)
|
|
return oriented
|
|
|
|
def animate_logo(self):
|
|
"""
|
|
Reset the logo animation to its beginning and start animating.
|
|
"""
|
|
# Place the logo
|
|
margin = self.configuration.get("logo", "margin")
|
|
self.logo_text.location.center = self.get_display_surface().get_rect().center
|
|
self.logo_text.move(-self.orient(self.logo_fire.location).w + margin / 2, rotated=self.rotated)
|
|
|
|
# Clear the screen
|
|
self.get_display_surface().fill(self.configuration.get("display", "clear"))
|
|
|
|
# Place the fire at the left side of the logo text
|
|
if not self.rotated:
|
|
self.logo_fire.location.midleft = self.logo_text.location.midleft
|
|
else:
|
|
self.logo_fire.location.midbottom = self.logo_text.location.midbottom
|
|
|
|
# Close the draw clip for the logo completely
|
|
self.logo_text_clip = self.logo_text.location.copy()
|
|
if not self.rotated:
|
|
self.logo_text_clip.width = 0
|
|
else:
|
|
self.logo_text_clip.height = 0
|
|
|
|
# Queue the text reveal to start
|
|
self.play(self.reveal_text, delay=self.configuration.get("logo", "delay"))
|
|
|
|
def reveal_text(self):
|
|
"""
|
|
Move the fire right, opening the logo text clip rect until it's big enough to show all the text. Queue the animation to restart once finished.
|
|
"""
|
|
self.logo_fire.move(10, rotated=self.rotated)
|
|
halt = False
|
|
if not self.rotated:
|
|
self.logo_text_clip.width = self.logo_fire.location.left - self.logo_text.location.left
|
|
limit = self.logo_text.location.right + self.configuration.get("logo", "margin") / 2
|
|
if self.logo_fire.location.left > limit:
|
|
self.logo_fire.location.left = limit
|
|
halt = True
|
|
else:
|
|
self.logo_text_clip.height = self.logo_text.location.bottom - self.logo_fire.location.bottom
|
|
self.logo_text_clip.bottom = self.logo_text.location.bottom
|
|
limit = self.logo_text.location.top - self.configuration.get("logo", "margin") / 2
|
|
if self.logo_fire.location.bottom < limit:
|
|
self.logo_fire.location.bottom = limit
|
|
halt = True
|
|
if halt:
|
|
self.halt(self.reveal_text)
|
|
self.play(self.animate_logo, delay=self.configuration.get("logo", "restart"), play_once=True)
|
|
|
|
def respond(self, event):
|
|
"""
|
|
Respond to all PGFW commands. If an any button is pressed, launch the iBitFit game.
|
|
"""
|
|
if self.delegate.compare(event, "any"):
|
|
os.chdir(pathlib.Path("games/ibitfit"))
|
|
print(f"current working dir is {os.getcwd()}")
|
|
ibitfit = ElectricSieve(suppress_gpio_init=True, rotate=self.rotated)
|
|
ibitfit.display.set_screen(dimensions=ibitfit.configuration.get("display", "dimensions"))
|
|
ibitfit.run()
|
|
os.chdir(pathlib.Path("../.."))
|
|
self.display.set_screen(dimensions=self.configuration.get("display", "dimensions"))
|
|
print(f"current working dir is {os.getcwd()}")
|
|
|
|
def update(self):
|
|
pgfw.Animation.update(self)
|
|
|
|
# Temporary fix: this stops audio when the game is quit and prevents spawning multiple BGM when game is relaunched
|
|
pygame.mixer.stop()
|
|
|
|
# Erase the center of the screen if enabled
|
|
if not self.configuration.get("logo", "trail"):
|
|
erase_rect = self.logo_text.location.copy()
|
|
if not self.rotated:
|
|
erase_rect.width = self.logo_fire.location.right - self.logo_text.location.left
|
|
else:
|
|
erase_rect.height = self.logo_text.location.bottom - self.logo_fire.location.top
|
|
erase_rect.bottom = self.logo_text.location.bottom
|
|
self.get_display_surface().fill(self.configuration.get("display", "clear"), erase_rect)
|
|
|
|
# Draw the logo text clipped to the current state of the animation, then remove the clip
|
|
self.get_display_surface().set_clip(self.logo_text_clip)
|
|
self.logo_text.update()
|
|
self.get_display_surface().set_clip(None)
|
|
|
|
# Draw the fire
|
|
self.logo_fire.update()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Parse command line arguments
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--keep-working-directory", action="store_true", help="""
|
|
Keep using the current directory as the working directory. The script expects the games to be in the games/ folder, so they can be imported
|
|
as Python modules. Changing the directory to anything other than the launcher project's root folder will break it under normal circumstances,
|
|
so use with care.
|
|
""")
|
|
parser.add_argument(
|
|
"--kms", action="store_true", help="""
|
|
Use SDL 2's KMS video driver. For use on systems without a windowing system (like Linux in only console mode). See
|
|
https://wiki.libsdl.org/SDL2/FAQUsingSDL#how_do_i_choose_a_specific_video_driver
|
|
""")
|
|
parser.add_argument(
|
|
"--framebuffer", action="store_true", help="""
|
|
Use SDL 1.2's framebuffer video driver. For use on older systems without a windowing system that aren't using KMS. This won't work with
|
|
SDL 2 or Pygame 2. See https://wiki.libsdl.org/SDL2/FAQUsingSDL#how_do_i_choose_a_specific_video_driver
|
|
""")
|
|
parser.add_argument(
|
|
"--ignore-hangup", action="store_true", help="""
|
|
Ignore hangup signals. Enabling this may be necessary for running the launcher as a systemd service. See
|
|
https://stackoverflow.com/questions/57205271/how-to-display-pygame-framebuffer-using-systemd-service
|
|
""")
|
|
arguments, unknown = parser.parse_known_args()
|
|
|
|
# If not keeping the working directory, try to move into the same directory as where this program is stored. Use the location of the program
|
|
# that launched this process to determine the path.
|
|
if not arguments.keep_working_directory:
|
|
target = os.path.dirname(sys.argv[0])
|
|
if not pathlib.Path(os.getcwd()).samefile(target):
|
|
try:
|
|
os.chdir(target)
|
|
except:
|
|
print(f"""
|
|
Warning: detected that the working directory is not the same as the launcher project's root directory and could not change to
|
|
to detected directory {target}
|
|
""")
|
|
|
|
if arguments.kms:
|
|
|
|
# Use the KMS video driver. This works for newer versions of Raspberry Pi with the KMS overlay enabled, SDL 2, and Pygame 2.
|
|
os.putenv("SDL_VIDEODRIVER", "kmsdrm")
|
|
|
|
elif arguments.framebuffer:
|
|
|
|
# Use the framebuffer display. This only works with Pygame 1.9.6 (and SDL 1.2).
|
|
os.putenv("SDL_VIDEODRIVER", "fbcon")
|
|
os.putenv("SDL_FBDEV", "/dev/fb0")
|
|
|
|
# Run the launcher
|
|
playzing = Playzing()
|
|
playzing.run()
|