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 69 commits into
base: content
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
69 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
4aa1796
fix: use old style annotation
Booplicate Feb 10, 2025
ed7dd50
fix: renpy doesn't use Formatter anymore
Booplicate Feb 10, 2025
ba6efbe
fix: use old style annotations
Booplicate Feb 10, 2025
2d7dc41
build: bump renpy to 8.3.4
Booplicate Feb 10, 2025
404805c
fix: add debug print
Booplicate Feb 10, 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
8 changes: 8 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 Down Expand Up @@ -35,3 +36,10 @@ zzzz*
cacert.pem
navigation.json
Monika_After_Story-[0-9]*.[0-9]*.[0-9]*-dists

# Development
.env
Monika After Story/game/Submods

# Other
*.disabled
289 changes: 289 additions & 0 deletions Monika After Story/game/000loader.rpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
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)
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_OR_SACRIFICE_CHILDREN"
__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 __set_dev_pm_var():
store.persistent._mas_pm_used_dm = True

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

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

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()
12 changes: 9 additions & 3 deletions Monika After Story/game/0config.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -35,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 @@ -132,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 @@ -157,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
10 changes: 10 additions & 0 deletions Monika After Story/game/0utils.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -747,3 +747,13 @@ python early in mas_utils:
return int(value)
except:
return default

# Override to completely disable Ren'Py's signature verification """f e a t u r e"""
# NOTE: Without this, Ren'Py will literally replace the persistent data with a blank file if signature verification fails.
# And the game will not inform the user, making backups/transfers impossible.
# Btw this apparently comes with the tagline "There is intentionally no way to disable this feature." lol
python early:
def verify_data_override(data, signatures, check_verifying=True):
return True

renpy.savetoken.verify_data = verify_data_override
2 changes: 1 addition & 1 deletion Monika After Story/game/chess.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -2683,7 +2683,7 @@ init python:
if ret_value is not None:
return ret_value

elif config.developer and ev.type == pygame.KEYDOWN:
elif ev.type == pygame.KEYDOWN and store._mas_root.is_dm_enabled():
# debug keys for dev testing
if ev.key == pygame.K_d:
# toggle draw button state
Expand Down
5 changes: 1 addition & 4 deletions Monika After Story/game/definitions.rpy
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
define persistent.demo = False

define config.developer = False
# define persistent.steam = "steamapps" in config.basedir.lower()


python early:
# We want these to be available globally, please don't remove
Expand Down Expand Up @@ -7500,7 +7497,7 @@ define n = DynamicCharacter('n_name', image='natsuki', what_prefix='"', what_suf
define y = DynamicCharacter('y_name', image='yuri', what_prefix='"', what_suffix='"', ctc="ctc", ctc_position="fixed")
define ny = Character('Nat & Yuri', what_prefix='"', what_suffix='"', ctc="ctc", ctc_position="fixed")

define _dismiss_pause = config.developer
define _dismiss_pause = store._mas_root.is_dm_enabled()

default persistent.playername = ""
default player = persistent.playername
Expand Down
2 changes: 1 addition & 1 deletion Monika After Story/game/dev/dev_d25_resets.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

init 999 python:
if persistent._mas_override_d25_gift_react is None:
persistent._mas_override_d25_gift_react = config.developer
persistent._mas_override_d25_gift_react = store._mas_root.is_dm_enabled()

init 998 python:
def mas_reset_d25():
Expand Down
2 changes: 1 addition & 1 deletion Monika After Story/game/dev/dev_farewells.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

init python:
if persistent._mas_fastbye is None:
persistent._mas_fastbye = config.developer
persistent._mas_fastbye = store._mas_root.is_dm_enabled()


init 5 python:
Expand Down
Loading