Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New submod framework #9742

Open
wants to merge 64 commits into
base: content
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3e8473f
wip: new submod framework
Booplicate Sep 29, 2022
b40ea0d
smol fixes to types, comment out obsolete code
Booplicate Sep 30, 2022
d238c49
control script loading
Booplicate Nov 11, 2022
1f3ef04
req changes
Booplicate Nov 11, 2022
44be6c5
submod framework improvements
Booplicate Nov 12, 2022
9c90cc3
sanity check setting pane exists
Booplicate Nov 12, 2022
6522796
store directory
Booplicate Nov 12, 2022
d3fb9fb
more wip submod framework
Booplicate Nov 12, 2022
e1681dc
allow loading submods
Booplicate Nov 12, 2022
92e0bb3
implement `include_module`
Booplicate Nov 12, 2022
df1ae01
sort before loading
Booplicate Nov 12, 2022
0c2e235
support submods py-packs
Booplicate Nov 12, 2022
fbafa7d
implement `import_from_path`
Booplicate Nov 12, 2022
4809c47
import in submod utils
Booplicate Nov 12, 2022
11c4845
update places where config was used
Booplicate Nov 12, 2022
72f2eec
more validation + fixes
Booplicate Nov 13, 2022
de71012
add `pydantic`
Booplicate Nov 15, 2022
adf64ba
use `pydantic` for json validation
Booplicate Nov 15, 2022
892b2ed
make class private + fixes
Booplicate Nov 15, 2022
efde52a
smol
Booplicate Nov 15, 2022
469e526
dont promote using github
Booplicate Jan 22, 2023
50103d6
cleanup
Booplicate Jan 22, 2023
934fecc
handle ren_py
Booplicate Jan 22, 2023
2f3ddeb
update module loading func
Booplicate Jan 23, 2023
3d47c03
fix typo that lead to nameerror
Booplicate Jan 23, 2023
b880194
Merge remote-tracking branch 'upstream/content' into new_submod_frame…
multimokia Jun 10, 2023
52078c8
add .env support + fix indentation
multimokia Jun 10, 2023
c15d93d
Add sample submod in testcases
multimokia Jun 13, 2023
bb0bed9
use renpy.config.gamedir here
multimokia Jun 14, 2023
1a97dc1
never upload .disabled
multimokia Jun 14, 2023
2e4a058
fix renpy's "feature" of save """security"""
multimokia Jun 15, 2023
4b9262e
impl submod settings
Booplicate Jun 19, 2023
b0e88c4
fix return type
Booplicate Jun 19, 2023
ceb2eab
fix: use default with persistent
Booplicate Jun 19, 2023
3365c93
add early_developer to loader
Booplicate Jun 19, 2023
8593555
remove our ssl/certifi packages and add a todo
multimokia Jun 21, 2023
666f4d1
fix push/queue event
multimokia Jun 21, 2023
e0a26ed
use constrained types for simple validation
multimokia Jun 21, 2023
26b4e6d
submod schema to v1
multimokia Jun 21, 2023
57814b0
improve naming
Booplicate Jun 21, 2023
ef0e1e1
smol readability
Booplicate Jun 21, 2023
ce4af7e
fix import for modules
Booplicate Jun 21, 2023
2435378
hide private field from schema
Booplicate Jun 21, 2023
4b7bc71
impl platform check + refactor attr of Submod cls
Booplicate Jun 21, 2023
6a4989f
reduce nesting
Booplicate Jun 21, 2023
9ce7e1b
swap methods around
Booplicate Jun 21, 2023
e6c6e86
better naming
Booplicate Jun 21, 2023
a08c5a4
disable broken submods + actually use settings
Booplicate Jun 21, 2023
dbf0f50
change platform check
Booplicate Jun 23, 2023
7acf001
improving schemas + validation (WIP)
multimokia Jun 24, 2023
bae31ef
fix for attr starting with underscore
Booplicate Jun 24, 2023
9fca6ad
multiple fixes
Booplicate Jun 25, 2023
97ad925
fix ci
Booplicate Jul 9, 2023
dc5754c
try ci w/o outdated rpycs
Booplicate Jul 9, 2023
2d22cf2
Merge branch 'content' into new_submod_framework
ThePotatoGuy Feb 21, 2024
65f96e4
Merge branch 'content' into new_submod_framework
ThePotatoGuy Feb 24, 2024
e1197a5
update gitignore for pycharm
ThePotatoGuy Feb 25, 2024
80b663d
add renpy spec for renpy typing in py packages (slow wip)
ThePotatoGuy Feb 25, 2024
a0a326f
remove the children part
ThePotatoGuy Feb 25, 2024
0e9fbac
move can import and threading into py packages, gut can import since …
ThePotatoGuy Feb 25, 2024
932ac02
drop the dataclass part since its not needed
ThePotatoGuy Feb 25, 2024
5f8d897
delete renpyspec and import renpy
ThePotatoGuy Feb 25, 2024
6753928
update ci env var
ThePotatoGuy Feb 25, 2024
4715532
fix: rm unused, this override moved to overrides.rpy
Booplicate Jan 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/mas_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ jobs:
build:
name: ci_build

environment: ci

# The type of runner that the job will run on
runs-on: ubuntu-20.04

env:
I_AM_RESPONSIBLE_FOR_ALL_ISSUES_AND_WILLING_TO_VOID_MY_WARRANTY_AND_SUPPORT: ${{ secrets.DEV_MODE }}
MAS_RENPY_VER: 8.1.1
MAS_BASE_DIR: masbase
MAS_DIR: mas
Expand Down
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#Catch all rpa, rpyc, rpyb, and rpyms
*.rpa
*.rpy[cbm]
*.rpymc

#Catch any persist info
persistent
Expand All @@ -14,6 +15,7 @@ __pycache__
*.swp
**/*.vscode
tools/*
.idea/

#Various other clutter-y things
log.txt
Expand All @@ -36,3 +38,13 @@ zzzz*
cacert.pem
navigation.json
Monika_After_Story-[0-9]*.[0-9]*.[0-9]*-dists

# Development
.env
Monika After Story/game/Submods
Monika After Story/game/saves
venv/

# Other
*.disabled
Monika After Story/characters
296 changes: 296 additions & 0 deletions Monika After Story/game/000loader.rpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
python early in _mas_loader:
import os
import glob
import sys
from itertools import chain
from heapq import merge as heapq_merge
from importlib.util import (
spec_from_file_location,
module_from_spec
)
from collections.abc import Iterator
from types import ModuleType

import store
from renpy.game import script as renpy_script


__EXCEPTIONS = frozenset((
"images.rpa",
"audio.rpa",
"fonts.rpa",
"scripts.rpa"
))

__GLOB_PATTERN_0 = "**/*.rp[aey]*"
__GLOB_PATTERN_1 = "**/*_ren.py"

__RS_EXTS = frozenset((
"rpa",
"rpe",
"rpy",
"rpyc",
"py"
))

__DISB_EXT = ".disabled"


class IncludeModuleError(Exception):
"""
Custom exception used by include_module
"""
def __init__(self, msg: str):
self.msg = msg

def __str__(self):
return self.msg


def _sanitise_path(path: str) -> str:
"""
Fixes filepaths on Windows OS

IN:
path - the path to fix

"""
return path.replace("\\", "/")

def _get_unrecognised_scripts() -> Iterator[str]:
"""
Returns an iterator over found unrecognised scripts
"""
gamedir = renpy.config.gamedir
file_names = chain(
glob.iglob(os.path.join(gamedir, __GLOB_PATTERN_0), recursive=True),
glob.iglob(os.path.join(gamedir, __GLOB_PATTERN_1), recursive=True)
)

for fn in file_names:
rel_fn = fn.partition(gamedir)[-1]
if rel_fn.startswith("\\") or rel_fn.startswith("/"):
rel_fn = rel_fn[1:]

if rel_fn in __EXCEPTIONS:
continue

ext = os.path.splitext(fn)[1]

if ext and ext[1:] in __RS_EXTS:
yield fn

def do_modules_exist(*modules: str, is_any: bool = False) -> bool:
"""
Checks if all or any of the modules were defined
NOTE: This doesn't validate if the modules are
loadable or valid at all

IN:
*modules - str, the modules to find
is_any - bool, check if any given module exists
instead of all
(Default: False)

OUT:
bool
"""
modules = set(modules)

for n, p in renpy_script.module_files:
if n in modules:
modules.remove(n)
if is_any or not modules:
return True

return False

def include_module(name: str):
"""
Fine, I'll do it myself (c)
Includes a module to load down the init pipeline

IN:
name - str, name of the moduleto include

RAISES:
IncludeModuleError - in case we failed to include the module for any reason
"""
if not renpy.is_init_phase():
raise IncludeModuleError("Can't include module when init phase is over")

try:
if not (module_initcode := renpy_script.load_module(name)):
# Loaded, but the module is empty, can quit here
return

except Exception as e:
raise IncludeModuleError(f"Failed to include module: {e}") from e

# We may not insert elements at or prior the current id!
current_id = renpy.game.initcode_ast_id

if module_initcode[0][0] < renpy_script.initcode[current_id][0]:
raise IncludeModuleError(
f"Module '{name}' contains nodes with priority lower than the node that loads it"
)

merge_id = current_id + 1
current_tail = renpy_script.initcode[merge_id:]
# Since script initcode and module initcode are both sorted,
# we can use heap to merge them
new_tail = heapq_merge(current_tail, module_initcode, key=lambda i: i[0])

renpy_script.initcode[merge_id:] = list(new_tail)

def _disable_unrecognised_scripts():
"""
Iterates over unrecognised scripts and disables them
so they won't be loaded next time

RAISES:
RuntimeError - in case we failed an OS call
"""
for fn in _get_unrecognised_scripts():
try:
os.rename(fn, "{}{}".format(fn, __DISB_EXT))

except OSError as e:
raise RuntimeError(
f"Unrecognised script at '{fn}'\nPlease remove the script manually"
) from e

def _unload_unrecognised_scripts():
"""
Iterates over unrecognised scripts and unloads them
"""
scripts = renpy_script.script_files

for i in range(len(scripts)-1, -1, -1):
name, path = scripts[i]

if (
path is None# Means packed
or path.endswith("/renpy/common")# renpy specific
):
continue

scripts.pop(i)

def import_from_path(name: str, path: str, *, is_global: bool = False) -> ModuleType:
"""
Dynamically imports a module from the given relative path
This is like Nodejs 'require'

Example:
my_module = import_from_path("my_module", "some/path/my_module.py")
my_module.hello_world()

IN:
name - str, the name to import the mode as
path - str, relative path to the module (relative to gamedir)
is_global - bool, whether or not add the module to 'sys.modules'
(Default: False)

OUT:
the module object

RAISES:
ModuleNotFoundError - if failed to find the module
"""
path = os.path.join(renpy.config.gamedir, path)
# If it's a dir, then it's a module, so we should find its __init__.py
if os.path.isdir(path):
path = os.path.join(path, "__init__.py")

spec = spec_from_file_location(name, path)
if spec is None:
raise ModuleNotFoundError(f"Failed to dynamically import '{path}' as '{name}', not found")

module = module_from_spec(spec)

if is_global:
sys.modules[name] = module

spec.loader.exec_module(module)

return module

def handle_scripts():
if not store._mas_root.is_dm_enabled():
_unload_unrecognised_scripts()
_disable_unrecognised_scripts()


python early in _mas_root:
import os
from dotenv import load_dotenv
import store

__ENV_FILE = f"{renpy.config.basedir}/.env"
load_dotenv(dotenv_path=__ENV_FILE, verbose=True)

__ENV_KEY = "I_AM_RESPONSIBLE_FOR_ALL_ISSUES_AND_WILLING_TO_VOID_MY_WARRANTY_AND_SUPPORT"
__DM_ENV_VALUE = "Yes, I will regret this! Enable DM!"
__CNSL_ENV_VALUE = "Yes, I will regret this! Enable CNSL!"


def __get_env_var(key: str) -> str|None:
"""
Gets value of an env variable
"""
return os.environ.get(key, None)

def is_dm_enabled() -> bool:
"""
Checks if dm is enabled
"""
return __get_env_var(__ENV_KEY) == __DM_ENV_VALUE

def _is_cnsl_enabled() -> bool:
"""
Checks if cnsl is enabled
"""
return __get_env_var(__ENV_KEY) == __CNSL_ENV_VALUE

def __mark_dm():
store.persistent._mas_pm_used_dm = True

def __dm_enabled_cb():
"""
Callback on dm enabling
"""
renpy.config.developer = True
renpy.config.early_developer = True
renpy.config.console = True
__mark_dm()

def __dm_disabled_cb():
"""
Callback on dm disabling
"""
renpy.config.developer = False
renpy.config.early_developer = False
if _is_cnsl_enabled():
renpy.config.console = True
__mark_dm()

else:
renpy.config.console = False

def handle_dm() -> bool:
if is_dm_enabled():
__dm_enabled_cb()
return True

__dm_disabled_cb()
return False


python early:
_mas_loader.handle_scripts()

init -999 python:
# This has to be run during init,
# and perhaps no earlier than -999
_mas_root.handle_dm()
13 changes: 9 additions & 4 deletions Monika After Story/game/0config.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ python early:
## The version of the game.
renpy.config.version = "0.13.0"


#Triple space suffix to avoid potential issues with same names in window title
config.window_title = "Monika After Story "

Expand All @@ -36,7 +35,8 @@ python early:

renpy.config.save_directory = "Monika After Story"

### R7+ Config Var adjustments
### R8+ Config Var adjustments

## 7.4.11
renpy.config.mouse_focus_clickthrough = True
##7.3.3
Expand Down Expand Up @@ -133,9 +133,9 @@ init -1200 python:
renpy.config.autosave_slots = 0
renpy.config.layers = ["master", "transient", "minigames", "screens", "overlay", "front"]
renpy.config.image_cache_size = 64
renpy.config.debug_image_cache = config.developer
renpy.config.debug_image_cache = False
renpy.config.predict_statements = 5
renpy.config.rollback_enabled = config.developer
renpy.config.rollback_enabled = False
renpy.config.menu_clear_layers = ["front"]
renpy.config.gl_test_image = "white"

Expand All @@ -158,3 +158,8 @@ define config.window_hide_transition = dissolve_textbox

init python:
config.per_frame_screens.append("_trace_screen")

init -1099 python:
## 8.1 Disable syncing
## NOTE: MUST BE AFTER INIT -1100
renpy.config.has_sync = False
Loading