playzing/Playzing.py

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()