1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
2#
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation files
5# (the "Software"), to deal in the Software without restriction,
6# including without limitation the rights to use, copy, modify, merge,
7# publish, distribute, sublicense, and/or sell copies of the Software,
8# and to permit persons to whom the Software is furnished to do so,
9# subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be
12# included in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22# This module is intended to be used as a singleton object.
23# It's purpose is to store in one global all of the data that would
24# be to annoying to lug around otherwise.
25
26from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
27from renpy.compat import *
28
29import renpy.display
30
31# The basepath.
32basepath = None
33
34# A list of paths that we search to load things. This is searched for
35# everything that can be loaded, before archives are used.
36searchpath = [ ]
37
38# The options that were read off the command line.
39args = None
40
41# The game's script.
42script = None
43
44# A stack of execution contexts.
45contexts = [ ]
46
47# The interface that the game uses to interact with the user.
48interface = None
49
50# Are we inside lint?
51lint = False
52
53# The RollbackLog that keeps track of changes to the game state
54# and to the store.
55log = None
56
57# Some useful additional information about program execution that
58# can be added to the exception.
59exception_info = ''
60
61# Used to store style information.
62style = None
63
64# The set of statements we've seen in this session.
65seen_session = { }
66
67# The number of entries in persistent._seen_translates that are also in
68# the current game.
69seen_translates_count = 0
70
71# The number of new translates we've seen today.
72new_translates_count = 0
73
74# True if we're in the first interaction after a rollback or rollforward.
75after_rollback = False
76
77# Code that's run after the init code.
78post_init = [ ]
79
80# Should we attempt to run in a mode that uses less memory?
81less_memory = False
82
83# Should we attempt to run in a mode that minimizes the number
84# of screen updates?
85less_updates = False
86
87# Should we never show the mouse?
88less_mouse = False
89
90# Should we not imagedissiolve?
91less_imagedissolve = False
92
93# The persistent data that's kept from session to session
94persistent = None
95
96# The current preferences.
97preferences = None
98
99
100class ExceptionInfo(object):
101    """
102    Context manager that sets exception_info iff an exception occurs.
103
104    `s`
105        A percent-format string to use.
106    `args`
107        The arguments that are percent-formatted with `s`.
108    """
109
110    def __init__(self, s, args):
111        self.s = s
112        self.args = args
113
114    def __enter__(self):
115        return
116
117    def __exit__(self, exc_type, exc_val, exc_tb):
118        if exc_type:
119            renpy.game.exception_info = self.s % self.args
120
121        return False
122
123
124class RestartContext(Exception):
125    """
126    Restarts the current context. If `label` is given, calls that label
127    in the restarted context.
128    """
129
130
131class RestartTopContext(Exception):
132    """
133    Restarts the top context. If `label` is given, calls that label
134    in the restarted context.
135    """
136
137
138class FullRestartException(Exception):
139    """
140    An exception of this type forces a hard restart, completely
141    destroying the store and config and so on.
142    """
143
144    def __init__(self, reason="end_game"): # W0231
145        self.reason = reason
146
147
148class UtterRestartException(Exception):
149    """
150    An exception of this type forces an even harder restart, causing
151    Ren'Py and the script to be reloaded.
152    """
153
154
155class QuitException(Exception):
156    """
157    An exception of this class will let us force a safe quit, from
158    anywhere in the program.
159
160    `relaunch`
161        If given, the program will run another copy of itself, with the
162        same arguments.
163
164    `status`
165        The status code Ren'Py will return to the operating system.
166    """
167
168    def __init__(self, relaunch=False, status=0):
169        Exception.__init__(self)
170        self.relaunch = relaunch
171        self.status = status
172
173
174class JumpException(Exception):
175    """
176    This should be raised with a label as the only argument. This causes
177    the current statement to terminate, and execution to be transferred
178    to the named label.
179    """
180
181
182class JumpOutException(Exception):
183    """
184    This should be raised with a label as the only argument. This exits
185    the current context, and then raises a JumpException.
186    """
187
188
189class CallException(Exception):
190    """
191    Raise this exception to cause the current statement to terminate,
192    and control to be transferred to the named label.
193    """
194
195    from_current = False
196
197    def __init__(self, label, args, kwargs, from_current=False):
198        Exception.__init__(self)
199
200        self.label = label
201        self.args = args
202        self.kwargs = kwargs
203        self.from_current = from_current
204
205    def __reduce__(self):
206        return (CallException, (self.label, self.args, self.kwargs, self.from_current))
207
208
209class EndReplay(Exception):
210    """
211    Raise this exception to end the current replay (the current call to
212    call_replay).
213    """
214
215
216class ParseErrorException(Exception):
217    """
218    This is raised when a parse error occurs, after it has been
219    reported to the user.
220    """
221
222
223# A tuple of exceptions that should not be caught by the
224# exception reporting mechanism.
225CONTROL_EXCEPTIONS = (
226    RestartContext,
227    RestartTopContext,
228    FullRestartException,
229    UtterRestartException,
230    QuitException,
231    JumpException,
232    JumpOutException,
233    CallException,
234    EndReplay,
235    ParseErrorException,
236    KeyboardInterrupt,
237    )
238
239
240def context(index=-1):
241    """
242    Return the current execution context, or the context at the
243    given index if one is specified.
244    """
245
246    return contexts[index]
247
248
249def invoke_in_new_context(callable, *args, **kwargs): # @ReservedAssignment
250    """
251    :doc: label
252
253    This function creates a new context, and invokes the given Python
254    callable (function) in that context. When the function returns
255    or raises an exception, control returns to the the original context.
256    It's generally used to call a Python function that needs to display
257    information to the player (like a confirmation prompt) from inside
258    an event handler.
259
260    A context maintains the state of the display (including what screens
261    and images are being shown) and the audio system. Both are restored
262    when the context returns.
263
264    Additional arguments and keyword arguments are passed to the
265    callable.
266
267    A context created with this function cannot execute Ren'Py script.
268    Functions that would change the flow of Ren'Py script, like
269    :func:`renpy.jump`, are handled by the outer context. If you want
270    to call Ren'Py script rather than a Python function, use
271    :func:`renpy.call_in_new_context` instead.
272    """
273
274    restart_context = False
275
276    context = renpy.execution.Context(False, contexts[-1], clear=True)
277    contexts.append(context)
278
279    if renpy.display.interface is not None:
280        renpy.display.interface.enter_context()
281
282    try:
283
284        return callable(*args, **kwargs)
285
286    except renpy.game.RestartContext:
287        restart_context = True
288        raise
289
290    except renpy.game.RestartTopContext:
291        restart_context = True
292        raise
293
294    except renpy.game.JumpOutException as e:
295
296        contexts[-2].force_checkpoint = True
297        contexts[-2].abnormal = True
298        raise renpy.game.JumpException(e.args[0])
299
300    finally:
301
302        if not restart_context:
303            context.pop_all_dynamic()
304
305        contexts.pop()
306        contexts[-1].do_deferred_rollback()
307
308        if interface and interface.restart_interaction and contexts:
309            contexts[-1].scene_lists.focused = None
310
311
312def call_in_new_context(label, *args, **kwargs):
313    """
314    :doc: label
315
316    This creates a new context, and then starts executing Ren'Py script
317    from the given label in that context. Rollback is disabled in the
318    new context, and saving/loading will occur in the top level
319    context.
320
321    Use this to begin a second interaction with the user while
322    inside an interaction.
323    """
324
325    context = renpy.execution.Context(False, contexts[-1], clear=True)
326    contexts.append(context)
327
328    if renpy.display.interface is not None:
329        renpy.display.interface.enter_context()
330
331    if args:
332        renpy.store._args = args
333    else:
334        renpy.store._args = None
335
336    if kwargs:
337        renpy.store._kwargs = renpy.python.RevertableDict(kwargs)
338    else:
339        renpy.store._kwargs = None
340
341    try:
342
343        context.goto_label(label)
344        return renpy.execution.run_context(False)
345
346    except renpy.game.JumpOutException as e:
347        contexts[-2].force_checkpoint = True
348        contexts[-2].abnormal = True
349        raise renpy.game.JumpException(e.args[0])
350
351    finally:
352
353        contexts.pop()
354        contexts[-1].do_deferred_rollback()
355
356        if interface and interface.restart_interaction and contexts:
357            contexts[-1].scene_lists.focused = None
358
359
360def call_replay(label, scope={}):
361    """
362    :doc: replay
363
364    Calls a label as a memory.
365
366    Keyword arguments are used to set the initial values of variables in the
367    memory context.
368    """
369
370    renpy.game.log.complete()
371
372    old_log = renpy.game.log
373    renpy.game.log = renpy.python.RollbackLog()
374
375    sb = renpy.python.StoreBackup()
376    renpy.python.clean_stores()
377
378    context = renpy.execution.Context(True)
379    contexts.append(context)
380
381    if renpy.display.interface is not None:
382        renpy.display.interface.enter_context()
383
384    # This has to be here, to ensure the scope stuff works.
385    renpy.exports.execute_default_statement()
386
387    for k, v in renpy.config.replay_scope.items():
388        setattr(renpy.store, k, v)
389
390    for k, v in scope.items():
391        setattr(renpy.store, k, v)
392
393    renpy.store._in_replay = label
394
395    try:
396
397        context.goto_label("_start_replay")
398        renpy.execution.run_context(False)
399
400    except EndReplay:
401        pass
402
403    finally:
404
405        context.pop_all_dynamic()
406
407        contexts.pop()
408        renpy.game.log = old_log
409        sb.restore()
410
411        if interface and interface.restart_interaction and contexts:
412            contexts[-1].scene_lists.focused = None
413
414        renpy.config.skipping = None
415
416    if renpy.config.after_replay_callback:
417        renpy.config.after_replay_callback()
418
419
420# Type information.
421if False:
422    script = renpy.script.Script()
423    interface = renpy.display.core.Interface()
424    log = renpy.python.RollbackLog()
425    preferences = renpy.preferences.Preferences()
426