Skip to content

Commit

Permalink
Merge pull request #92 from hanzi/github-action
Browse files Browse the repository at this point in the history
Add a basic GitHub Actions workflow for releases
  • Loading branch information
40Cakes authored Oct 31, 2023
2 parents 335b2c2 + 6dcddc6 commit 4284a0b
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 218 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/release-when-tag-pushed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Creates a new release when a tag is pushed

on:
push:
tags:
- '*'

jobs:
create-release-for-tag:
# This release action only really makes sense in the main repository and not in
# a fork, hence this condition.
if: github.repository == '40Cakes/pokebot-gen3'

name: "Create a release for the tag"
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Update version.py and remove unnecessary files
run: |
echo "pokebot_name = \"PokéBot\"" > modules/version.py
echo "pokebot_version = \"${{ github.ref_name }}\"" >> modules/version.py
rm -rf .git .github .gitattributes .gitignore pokebot.spec
mv LICENSE LICENSE.txt
- name: Create a ZIP file
run: |
zip -qq -r /tmp/pokebot-${{ github.ref_name }}.zip .
- name: Creates the GitHub release
uses: marvinpinto/[email protected]
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
prerelease: false
automatic_release_tag: ${{ github.ref_name }}
files: |
/tmp/pokebot-${{ github.ref_name }}.zip
21 changes: 6 additions & 15 deletions import.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import binascii
import os
import shutil
import tkinter as tk
import zlib
from pathlib import Path
from tkinter import ttk, filedialog, font
from typing import IO, Union

from modules.game import set_rom, get_symbol, decode_string
from modules.memory import unpack_uint16, unpack_uint32
from modules.profiles import Profile, create_profile, profile_directory_exists
from modules.roms import list_available_roms
from modules.runtime import is_bundled_app, get_base_path
from modules.version import pokebot_name, pokebot_version


Expand Down Expand Up @@ -81,18 +80,6 @@ def migrate_save_state(file: IO) -> Profile:
with open(profile.path / "current_save.sav", "wb") as save_file:
save_file.write(savegame_data)

# In case this save state has been used with the old Pymem implementation of the bot, try to
# import config and stats into the new directory structure.
full_game_code = (matching_rom.game_code + str(matching_rom.language)).upper()
config_dir = Path(__file__).parent / "config" / full_game_code / f"{trainer_id}-{trainer_name}"
stats_dir = Path(__file__).parent / "stats" / full_game_code / f"{trainer_id}-{trainer_name}"

if config_dir.is_dir():
shutil.copytree(config_dir, profile.path / "profiles")

if stats_dir.is_dir():
shutil.copytree(stats_dir, profile.path / "stats")

file.close()

return profile
Expand Down Expand Up @@ -176,7 +163,7 @@ def handle_button_click() -> None:
]

file = filedialog.askopenfile(
title="Select Save State", initialdir=Path(__file__).parent, filetypes=filetypes, mode="rb"
title="Select Save State", initialdir=get_base_path(), filetypes=filetypes, mode="rb"
)

if file is not None:
Expand Down Expand Up @@ -210,6 +197,10 @@ def show_success_message(profile_name, game_name) -> None:


if __name__ == "__main__":
if not is_bundled_app():
from requirements import check_requirements
check_requirements()

window = tk.Tk()
window.title(f"Save Importer for {pokebot_name} {pokebot_version}")
window.geometry("480x320")
Expand Down
4 changes: 2 additions & 2 deletions modules/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ def _handle_copy(self) -> None:
if len(selection) < 1:
return

import pyperclip
import pyperclip3

pyperclip.copy(self._tv.item(selection[0])["values"][0])
pyperclip3.copy(str(self._tv.item(selection[0])["values"][0]))

def _handle_action(self, callback: callable) -> None:
selection = self._tv.selection()
Expand Down
7 changes: 7 additions & 0 deletions modules/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ def is_bundled_app() -> bool:
return getattr(sys, "frozen", False)


def is_virtualenv() -> bool:
"""
:return: Whether we are running in a virtualenv (True) or in the global Python environment (False)
"""
return sys.prefix != sys.base_prefix


def get_base_path() -> Path:
"""
:return: A `Path` object to the base directory of the bot (where `pokebot.py` or `pokebot.exe`
Expand Down
33 changes: 32 additions & 1 deletion modules/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
# Note: This file will get replaced when run in GitHub actions.
# In that case, the tagged version will be placed in here instead.
#
# So this file is only for development, or when someone just fetches
# the Git repository.
#
# It will try to get the current commit hash and use it as the
# version number, prefixed by `dev-` (e.g. `dev-a1b2c3d`.)

import os
from modules.runtime import get_base_path

pokebot_name = "PokéBot"
pokebot_version = "v0.0.3a" # TODO automatically append latest commit hash to version e.g. v0.0.1-e3fa5f5
pokebot_version = "dev"

try:
# If someone managed to get a copy of the repository without actually having
# Git installed, this should still be able to get the commit hash of the current
# HEAD.
# It's probably not the _best_ way to do this... but it works.
git_dir = get_base_path() / ".git"
if git_dir.is_dir():
with open(git_dir / "HEAD", "r") as head_file:
head = head_file.read().strip()
if head.startswith("ref: "):
full_path = git_dir.as_posix() + "/" + head[5:]
with open(full_path, "r") as ref_file:
ref = ref_file.read().strip()
pokebot_version = f"dev-{ref[0:7]}"
except:
# If any issue occurred while trying to figure out the current commit hash,
# just default to showing 'dev' for the version.
pass
174 changes: 18 additions & 156 deletions pokebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,11 @@
import argparse
import atexit
import platform
import sys
import json

from modules.runtime import is_bundled_app, get_base_path
from modules.version import pokebot_name, pokebot_version

OS_NAME = platform.system()

recommended_python_version = "3.12"
supported_python_versions = [(3, 10), (3, 11), (3, 12)]

libmgba_tag = "0.2.0-2"
libmgba_ver = "0.2.0"

required_modules = [
"numpy~=1.26.1",
"Flask~=2.3.2",
"Flask-Cors~=4.0.0",
"ruamel.yaml~=0.18.2",
"pypresence~=4.3.0",
"obsws-python~=1.6.0",
"pandas~=2.1.1",
"discord-webhook~=1.2.1",
"jsonschema~=4.17.3",
"rich~=13.5.2",
"cffi~=1.16.0",
"Pillow~=10.0.1",
"sounddevice~=0.4.6",
"requests~=2.31.0",
"pyperclip~=1.8.2",
]

if OS_NAME == "Windows":
required_modules.extend([
"pywin32>=306",
"psutil~=5.9.5"
])

gui = None


Expand All @@ -50,132 +17,23 @@
# For those cases, we register an `atexit` handler that will wait for user input before closing
# the terminal window.
def on_exit() -> None:
"""Windows-specific handler to prevent the terminal from being terminated until user input."""
import psutil
import os
parent_process_name = psutil.Process(os.getppid()).name()
if parent_process_name == "py.exe" or is_bundled_app():
if gui is not None and gui.window is not None:
gui.window.withdraw()

print("")
input("Press Enter to close...")


if OS_NAME == "Windows":
atexit.register(on_exit)


def check_requirements() -> None:
# We do not want to do downlaod requirements every single time the bot is started.
# As a quick sanity check, we store the current bot version in `.last-requirements-check`.
# If that file is present and contains the current bot version, we skip the check.
requirements_file = get_base_path() / ".last-requirements-check"
requirements_version_hash = pokebot_version + '/' + platform.python_version()
need_to_fetch_requirements = True
if requirements_file.is_file():
with open(requirements_file, "r") as file:
if file.read() == requirements_version_hash:
need_to_fetch_requirements = False
else:
print(
f"This is a newer version of {pokebot_name} than you have run before. "
f"Checking if requirements are met..."
)
print("")
else:
print(
f"Seems like this is the first time you are running {pokebot_name}!\n"
"Checking if requirements are met..."
)
print("")

if need_to_fetch_requirements:
print(f"The following Python modules need to be installed:\n\n{json.dumps(required_modules, indent=2)}\n\n")
response = input("Install modules? [y/n] ")
if response.lower() == "y":
python_version = platform.python_version_tuple()
version_matched = False
for supported_version in supported_python_versions:
if int(python_version[0]) == supported_version[0] and int(python_version[1]) == supported_version[1]:
version_matched = True
break
if not version_matched:
supported_versions_list = ", ".join(map(lambda t: f"{str(t[0])}.{str(t[1])}", supported_python_versions))
print(f"ERROR: The Python version you are using (Python {platform.python_version()}) is not supported.\n")
print(f"Supported versions are: {supported_versions_list}")
print(f"It is recommended that you install Python {recommended_python_version}.")
sys.exit(1)

# Some dependencies only work with 64-bit Python. Since this isn't the 90s anymore,
# we'll just require that.
if platform.architecture()[0] != "64bit":
print(f"ERROR: A 64-bit version of Python is required in order to run {pokebot_name} {pokebot_version}.\n")
print(f"You are currently running a {platform.architecture()[0]} version.")
sys.exit(1)

# Run `pip install` on all required modules.
import subprocess

pip_flags = ["--disable-pip-version-check", "--no-python-version-warning"]
for module in required_modules:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", *pip_flags, module],
stderr=sys.stderr, stdout=sys.stdout
)

# Make sure that `libmgba-py` is installed.
if OS_NAME == "Windows":
import psutil
import os
parent_process_name = psutil.Process(os.getppid()).name()
if parent_process_name == "py.exe" or is_bundled_app():
if gui is not None and gui.window is not None:
gui.window.withdraw()

print("")
libmgba_directory = get_base_path() / "mgba"
if not libmgba_directory.is_dir():
match platform.system():
case "Windows":
libmgba_url = (
f"https://github.com/hanzi/libmgba-py/releases/download/{libmgba_tag}/"
f"libmgba-py_{libmgba_ver}_win64.zip"
)

case "Linux":
linux_release = platform.freedesktop_os_release()
supported_linux_releases = [("ubuntu", "23.04"), ("debian", "12")]
if (linux_release["ID"], linux_release["VERSION_ID"]) not in supported_linux_releases:
print(
f'You are running an untested version of Linux ({linux_release["PRETTY_NAME"]}). '
f"Currently, only {supported_linux_releases} have been tested and confirmed working."
)
input("Press enter to install libmgba-py anyway...")
libmgba_url = (
f"https://github.com/hanzi/libmgba-py/releases/download/{libmgba_tag}/"
f"libmgba-py_{libmgba_ver}_ubuntu-lunar.zip"
)

case _:
print(f"ERROR: {platform.system()} is unsupported. Only Windows and Linux are currently supported.")
sys.exit(1)

import io
import requests
import zipfile

response = requests.get(libmgba_url)
if response.status_code == 200:
print("Unzipping libmgba into `./mgba`...")
with zipfile.ZipFile(io.BytesIO(response.content)) as zip_handle:
zip_handle.extractall(get_base_path())

# Mark the requirements for the current bot version as checked, so we do not
# have to run all of this again until the next update.
with open(requirements_file, "w") as file:
file.write(requirements_version_hash)

else:
print("Exiting!")
sys.exit(1)

print("")
input("Press Enter to close...")


atexit.register(on_exit)


def parse_arguments() -> argparse.Namespace:
"""Parses program arguments."""
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description=f'{pokebot_name} {pokebot_version}')
parser.add_argument(
'profile',
Expand All @@ -185,8 +43,10 @@ def parse_arguments() -> argparse.Namespace:
parser.add_argument('-d', '--debug', action='store_true', help='Enable extra debug options and a debug menu.')
return parser.parse_args()


if __name__ == "__main__":
if not is_bundled_app():
from requirements import check_requirements
check_requirements()

from modules.config import load_config_from_directory
Expand All @@ -203,12 +63,14 @@ def parse_arguments() -> argparse.Namespace:
if OS_NAME == "Windows":
import win32api


def win32_signal_handler(signal_type):
if signal_type == 2:
emulator = get_emulator()
if emulator is not None:
emulator.shutdown()


win32api.SetConsoleCtrlHandler(win32_signal_handler, True)

parsed_args = parse_arguments()
Expand Down
Loading

0 comments on commit 4284a0b

Please sign in to comment.