-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy path__init__.py
More file actions
369 lines (292 loc) · 13.2 KB
/
__init__.py
File metadata and controls
369 lines (292 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
"""This file is free software under the MIT license."""
#TODO - Modtools workshop item - fixed ID. Announcements to be made as metadata for that item. Show if changed?
import os
import sys
import renpy
try:
from steam_workshop.steam_config import has_steam
from steam_workshop.steamhandler import get_instance
get_instance()
except (ImportError, OSError):
workshop_enabled = False
def has_steam():
return False
else:
workshop_enabled = True
from types import ModuleType
import importlib
print 'AWSW Mod Loader Init'
def get_mod_path():
"""Get the mod path
Returns:
The full path to the mods folder
"""
return os.path.join(renpy.config.gamedir, "mods")
def report_exception(error):
if has_steam():
manager = get_instance()
# Steam only uploads minidumps after 10 exceptions for some reason
for i in range(11):
manager.HandleException(error)
def get_platform_name():
valid_paths = {"linux-i686", "linux-x86_64", "windows-i686", "darwin-x86_64"}
for path in valid_paths:
if any(path in i for i in sys.path):
return path
raise OSError("Modtools can't detect OS")
# Credit to Matthew for this code: https://stackoverflow.com/a/17194836/3398583
# Modified by Blue
def rreload(module, modules=None):
"""Recursively reloads a module.
Ignores modules in ``modules`` and all modules not in the mods folder
"""
print "RELOADING", module.__file__
sys.stdout.flush()
reload(module)
if modules is None:
modules = [module]
else:
modules.append(module)
for attribute_name in dir(module):
attribute = getattr(module, attribute_name)
if isinstance(attribute, ModuleType):
if attribute not in modules:
try:
if get_mod_path() in attribute.__file__:
rreload(attribute, modules)
except AttributeError:
# Seems to happen to sys randomly
pass
def is_github():
return renpy.loader.loadable("github.txt")
def test_command():
renpy.arguments.takes_no_arguments("Run internal modtools tests")
import testing.test as test
test.test_tests()
return False
def update_command():
from modloader.modconfig import update_modtools
zip_default = "https://github.com/AWSW-Modding/AWSW-Modtools/archive/develop.zip"
ap = renpy.arguments.ArgumentParser(description="Update the modtools.")
ap.add_argument("url", help="The URL to get the new version from. Must be a zip file.", nargs='?', default=zip_default)
args = ap.parse_args()
update_modtools(args.url)
def mod_error_check(func, mod_name, phase):
"""Reraise an exception with a name of the mod and its original traceback"""
try:
return func(mod_name)
except Exception as e:
original_msg = " " + type(e).__name__ + ": " + "\n ".join(str(e).split("\n"))
msg = "\nAn error occured while " + phase + " the mod \"" + mod_name + "\":\n\n" + original_msg + "\n\nPlease report this issue to the author of this mod."
raise Exception, Exception(msg), sys.exc_info()[2]
def report_mod_errors(errors): # NoReturn
"""
Reports a mod error to the user by displaying the modloader
error screen.
As the only actions available on the modloader error screen
are to "Reload" and "Quit", this function is guaranteed
not to return to the caller (except through Ren'Py's
control exceptions' unwinding.)
The structure of this function is taken from
renpy.display.error.report_parse_errors
"""
if not renpy.exports.has_screen("_modloader_errors"):
return True
renpy.display.error.init_display()
reload_action = renpy.exports.utter_restart
try:
renpy.game.invoke_in_new_context(
renpy.display.error.call_exception_screen,
"_modloader_errors",
reload_action=reload_action,
errors=errors,
)
except renpy.game.CONTROL_EXCEPTIONS:
raise
except:
renpy.display.log.write("While handling exception:")
renpy.display.log.exception()
raise
def report_modlist_errors(errors): # NoReturn
"""
Reports a modlist error to the user by displaying the modlist
error screen.
As the only actions available on the modlist error screen
are to "Reload" and "Quit", this function is guaranteed
not to return to the caller (except through Ren'Py's
control exceptions' unwinding.)
The structure of this function is taken from
report_mod_errors
"""
if not renpy.exports.has_screen("_modlist_errors"):
return True
renpy.display.error.init_display()
reload_action = renpy.exports.utter_restart
try:
renpy.game.invoke_in_new_context(
renpy.display.error.call_exception_screen,
"_modlist_errors",
reload_action=reload_action,
errors=errors,
)
except renpy.game.CONTROL_EXCEPTIONS:
raise
except:
renpy.display.log.write("While handling exception:")
renpy.display.log.exception()
raise
def resolve_dependencies():
"""Resolve mod dependencies and create mod load order"""
from modloader import modinfo
mod_load_order = []
load_later = []
incompatibilities = []
missing_dependencies = []
# loop through all imported mods
for mod_name, mod in modinfo.get_mods().iteritems():
# if no dependencies are specified we can just add the mod to mod_load_order
if not mod.dependencies:
mod_load_order.append(mod_name)
continue
raw_mod_deps = []
# check if all dependencies are imported and incompatible mods aren't
for mod_dep in mod.dependencies:
if mod_dep[0] == "!":
if modinfo.has_mod(mod_dep[1:]):
incompatibilities.append((mod_name, mod_dep[1:]))
elif mod_dep[0] == "?":
if modinfo.has_mod(mod_dep[1:]):
raw_mod_deps.append(mod_dep[1:])
elif modinfo.has_mod(mod_dep):
raw_mod_deps.append(mod_dep)
else:
missing_dependencies.append((mod_name, mod_dep))
# put all mods with dependencies to load_later
load_later.append((mod_name, raw_mod_deps))
if incompatibilities or missing_dependencies:
message = []
if incompatibilities:
message.append(
"Some of the mods installed conflict with each other and cannot be used together.\n"
"The following mods are incompatible:\n\n"
+ "".join(" {0!r} conflicts with {1!r}. Please remove one of them.\n".format(mod[0], mod[1]) for mod in incompatibilities)
)
if missing_dependencies:
message.append(
"Some of the mods installed require other mods which are not installed.\n"
"The following mods need to be installed:\n\n"
+ "".join(" {0!r} requires {1!r}. Please install {1!r}.\n".format(mod[0], mod[1]) for mod in missing_dependencies)
)
report_mod_errors("\n\n".join(message))
# NoReturn
# repeat until there's no mod left in load_later
while len(load_later) > 0:
new_load_later = []
# loop through mods which aren't in mod_load_order yet
for mod_name, mod_deps in load_later:
# get dependencies which still aren't in mod_load_order
later_deps = [mod_dep for mod_dep in mod_deps if mod_dep not in mod_load_order]
# if all dependencies are in mod_load_order, we append the mod to it as well
# otherwise we keep it in load_later and shrink the list of dependencies to only the ones which aren't in mod_load_order
if len(later_deps) > 0:
# the new load_later list is in reversed order to (hopefully) reduce the amount of loops needed
new_load_later.insert(0, (mod_name, later_deps))
else:
mod_load_order.append(mod_name)
# if no mod was moved from load_later to mod_load_order, we raise an error to prevent infinite loop
if len(new_load_later) == len(load_later):
#raise EnvironmentError("Failed resolving mod dependencies.\nThis may be caused by an occurance of cyclic dependency, which isn't allowed.")
report_mod_errors(
"I ran into an infinite loop while trying to figure out which mods need which other mods.\n"
"This is likely caused by a mod depending on itself or another mod which depends on it.\n"
"The following mods were affected. Please report this to their authors:\n\n"
+ "".join(" {0!r}\n".format(mod[0]) for mod in load_later)
)
load_later[:] = new_load_later
modinfo.mod_load_order[:] = mod_load_order
def main(reload_mods=False):
"""Load the mods"""
# Don't want to do this at the top because it breaks initial parse error handling.
from modloader import modinfo, modclass
from modloader.modconfig import report_duplicate_labels
#from steam_workshop.steam_config import sign_mod
#sign_mod(1597292073)
if reload_mods:
import modgame
rreload(modgame)
report_duplicate_labels()
# if has_steam():
# steammgr = get_instance()
# steammgr.CachePersonas()
# By appending the mod folder to the import path we can do something like
# `import test` to import the mod named test in the mod folder.
sys.path.append(get_mod_path())
# NOTE: To import files in the modloader/ folder, do
# `from modloader import ...`. If we add the modloader to the path,
# the modules would be reimported every time they are imported.
# In most cases, that's fine, but when modinfo is reimported, we lose the
# current list of mods.
# To run tests, do `python -O Angels with Scaly Wings.py . modtools_tests` in the AWSW root folder.
# Otherwise `.` has to be the path to the AWSW directory
renpy.arguments.register_command("modtools_tests", test_command)
renpy.arguments.register_command("modtools_update", update_command)
modinfo.reset_mods()
modules = []
valid_files = ['.DS_Store']
non_folders = []
for mod in modinfo.get_mod_folders():
if mod in valid_files:
modinfo.get_mod_folders().remove(mod)
continue
if not os.path.isdir(os.path.join(get_mod_path(), mod)):
non_folders.append(mod)
continue
# if the mod contains a "modules" folder add this folder to system path
modules_path = os.path.join(get_mod_path(), mod, "modules")
if os.path.isdir(modules_path):
sys.path.append(modules_path)
if non_folders:
report_mod_errors(
"The contents of the mods folder must all be themselves folders.\n"
"Zip files should be extracted into their own directory (like the core mod.)\n"
"The following items in your game/mods/ directory are not folders:\n\n"
+ "\n".join(" game/mods/{}".format(item) for item in non_folders)
)
# NoReturn
for mod in modinfo.get_mod_folders():
print "Begin mod import: {}".format(mod)
# Try importing the mod.
# Note: This doesn't give my mod functionality. To give the mod
# function, make a Mod class and apply the loadable_mod decorator
mod_object = mod_error_check(importlib.import_module, mod, "importing")
if reload_mods:
rreload(mod_object, modules)
# After all mods are imported, resolve their dependencies and create mod load order
resolve_dependencies()
# After load order is created, loop through the mods and add their resource folder to the front of search path list
for mod_name in modinfo.mod_load_order:
# Some mods require resources to be recognized by renpy.
# If a folder exists, force renpy to load it
resource_dir = os.path.join(modinfo.get_mod_path(mod_name), 'resource')
if os.path.isdir(resource_dir):
renpy.config.searchpath.insert(0, resource_dir)
# Then loop through the mods in their load order and call their respective mod_load functions
for mod_name in modinfo.mod_load_order:
print "Loading mod {}".format(mod_name)
mod_error_check(lambda n: modinfo.get_mod(n).mod_load(), mod_name, "loading")
# After all mods are loaded, call their respective mod_complete functions
for mod_name in modinfo.mod_load_order:
print "Completing mod {}".format(mod_name)
mod_error_check(lambda n: modinfo.get_mod(n).mod_complete(), mod_name, "completing")
# Force renpy to reindex all game files
renpy.loader.old_config_archives = None
# When we build the documentation, renpy.config.gamedir will not exist
# However, when the game is ran, it will exist. We take abuse of that
try:
game_dir = renpy.config.gamedir
BUILDING_DOCUMENTATION = False
except AttributeError:
BUILDING_DOCUMENTATION = True
else:
sys.path.append(os.path.join(renpy.config.gamedir, "modloader", "dll", get_platform_name()))
os.environ["SSL_CERT_FILE"] = os.path.join(game_dir, "modloader", "cacert.pem")