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# Functions that make the user's life easier.
23
24from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
25from renpy.compat import *
26
27
28import renpy.display
29import renpy.styledata
30import contextlib
31import time
32
33Color = renpy.color.Color
34color = renpy.color.Color
35
36
37def lookup_displayable_prefix(d):
38    """
39    Given `d`, a string given a displayable, returns the displayale it
40    corresponds to or None if it it does not correspond to one.
41    """
42
43    prefix, colon, arg = d.partition(":")
44
45    if not colon:
46        return None
47
48    fn = renpy.config.displayable_prefix.get(prefix, None)
49    if fn is None:
50        return None
51
52    return displayable(fn(arg))
53
54
55def displayable_or_none(d, scope=None, dynamic=True):
56
57    if isinstance(d, renpy.display.core.Displayable):
58        return d
59
60    if d is None:
61        return d
62
63    if isinstance(d, basestring):
64        if not d:
65            raise Exception("An empty string cannot be used as a displayable.")
66        elif ("[" in d) and renpy.config.dynamic_images and dynamic:
67            return renpy.display.image.DynamicImage(d, scope=scope)
68
69        rv = lookup_displayable_prefix(d)
70
71        if rv is not None:
72            return rv
73        elif d[0] == '#':
74            return renpy.store.Solid(d)
75        elif "." in d:
76            return renpy.store.Image(d)
77        else:
78            return renpy.store.ImageReference(tuple(d.split()))
79
80    if isinstance(d, Color):
81        return renpy.store.Solid(d)
82
83    if isinstance(d, list):
84        return renpy.display.image.DynamicImage(d, scope=scope)
85
86    # We assume the user knows what he's doing in this case.
87    if hasattr(d, '_duplicate'):
88        return d
89
90    if d is True or d is False:
91        return d
92
93    raise Exception("Not a displayable: %r" % (d,))
94
95
96def displayable(d, scope=None):
97    """
98    :doc: udd_utility
99    :name: renpy.displayable
100
101    This takes `d`, which may be a displayable object or a string. If it's
102    a string, it converts that string into a displayable using the usual
103    rules.
104    """
105
106    if isinstance(d, renpy.display.core.Displayable):
107        return d
108
109    if isinstance(d, basestring):
110        if not d:
111            raise Exception("An empty string cannot be used as a displayable.")
112        elif ("[" in d) and renpy.config.dynamic_images:
113            return renpy.display.image.DynamicImage(d, scope=scope)
114
115        rv = lookup_displayable_prefix(d)
116
117        if rv is not None:
118            return rv
119        elif d[0] == '#':
120            return renpy.store.Solid(d)
121        elif "." in d:
122            return renpy.store.Image(d)
123        else:
124            return renpy.store.ImageReference(tuple(d.split()))
125
126    if isinstance(d, Color):
127        return renpy.store.Solid(d)
128
129    if isinstance(d, list):
130        return renpy.display.image.DynamicImage(d, scope=scope)
131
132    # We assume the user knows what he's doing in this case.
133    if hasattr(d, '_duplicate'):
134        return d
135
136    if d is True or d is False:
137        return d
138
139    raise Exception("Not a displayable: %r" % (d,))
140
141
142def dynamic_image(d, scope=None, prefix=None, search=None):
143    """
144    Substitutes a scope into `d`, then returns a displayable.
145
146    If `prefix` is given, and a prefix has been given a prefix search is
147    performed until a file is found. (Only a file can be used in this case.)
148    """
149
150    if not isinstance(d, list):
151        d = [ d ]
152
153    def find(name):
154
155        if renpy.exports.image_exists(name):
156            return True
157
158        if renpy.loader.loadable(name):
159            return True
160
161        if lookup_displayable_prefix(name):
162            return True
163
164        if (len(d) == 1) and (renpy.config.missing_image_callback is not None):
165            if renpy.config.missing_image_callback(name):
166                return True
167
168    for i in d:
169
170        if not isinstance(i, basestring):
171            continue
172
173        if (prefix is not None) and ("[prefix_" in i):
174
175            if scope:
176                scope = dict(scope)
177            else:
178                scope = { }
179
180            for p in renpy.styledata.stylesets.prefix_search[prefix]:  # @UndefinedVariable
181                scope["prefix_"] = p
182
183                rv = renpy.substitutions.substitute(i, scope=scope, force=True, translate=False)[0]
184
185                if find(rv):
186                    return displayable_or_none(rv)
187
188                if search is not None:
189                    search.append(rv)
190
191        else:
192
193            rv = renpy.substitutions.substitute(i, scope=scope, force=True, translate=False)[0]
194
195            if find(rv):
196                return displayable_or_none(rv)
197
198            if search is not None:
199                search.append(rv)
200
201    else:
202
203        rv = d[-1]
204
205        if find(rv):
206            return displayable_or_none(rv, dynamic=False)
207
208        return None
209
210
211def predict(d):
212    d = renpy.easy.displayable_or_none(d)
213
214    if d is not None:
215        renpy.display.predict.displayable(d)
216
217
218@contextlib.contextmanager
219def timed(name):
220    start = time.time()
221    yield
222    print("{0}: {1:.2f} ms".format(name, (time.time() - start) * 1000.0))
223
224
225def split_properties(properties, *prefixes):
226    """
227    :doc: other
228
229    Splits up `properties` into multiple dictionaries, one per `prefix`. This
230    function checks each key in properties against each prefix, in turn.
231    When a prefix matches, the prefix is stripped from the key, and the
232    resulting key is mapped to the value in the corresponding dictionary.
233
234    If no prefix matches, an exception is thrown. (The empty string, "",
235    can be used as the last prefix to create a catch-all dictionary.)
236
237    For example, this splits properties beginning with text from
238    those that do not::
239
240        text_properties, button_properties = renpy.split_properties(properties, "text_", "")
241    """
242
243    rv = [ ]
244
245    for _i in prefixes:
246        rv.append({})
247
248    if not properties:
249        return rv
250
251    prefix_d = list(zip(prefixes, rv))
252
253    for k, v in properties.items():
254        for prefix, d in prefix_d:
255            if k.startswith(prefix):
256                d[k[len(prefix):]] = v
257                break
258        else:
259            raise Exception("Property {} begins with an unknown prefix.".format(k))
260
261    return rv
262