CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/display/image.py
2025-03-03 23:00:33 +01:00

1007 lines
26 KiB
Python

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# This file contains some miscellaneous displayables that involve images.
# Most of the guts of this file have been moved into im.py, with only some
# of the stuff thar uses images remaining.
from __future__ import print_function
import renpy.display
import renpy.text
from renpy.display.render import render, Render
import collections
# A map from image name to the displayable object corresponding to that
# image.
images = { }
# A map from image tag to lists of possible attributes for images with that
# tag.
image_attributes = collections.defaultdict(dict)
# The set of image names Ren'Py knows about, as strings with spaces.
image_names = [ ]
def list_images():
"""
:doc: image_func
Returns a list of images that have been added to Ren'Py, as a list of
strings with spaces between the name components.
"""
return image_names
def get_available_image_tags():
"""
:doc: image_func
Returns a list of image tags that have been defined.
"""
return [ k for k, v in image_attributes.items() if v ]
def get_available_image_attributes(tag, attributes=()):
"""
:undocumented:
Returns a list of tuples, with each tuple representing a possible
combination of image attributes that can be associated with `tag`.
If `attributes` is given, only images that contain all the attributes
in that iterable are returned.
"""
rv = [ ]
if tag not in image_attributes:
return rv
for at in image_attributes[tag]:
for a in attributes:
if a not in at:
break
else:
rv.append(at)
return rv
def get_tag_method(tag, method):
"""
This looks for an image named `tag`, then tries to grab the given method
from it. If it can do that, returns a reference to the method. Otherwise,
it returns None.
"""
ti = images.get((tag,), None)
if ti is None:
return None
return getattr(ti, method, None)
def check_image_attributes(tag, attributes):
"""
:doc: image_func
Checks to see if there is a unique image with the given tag and
attributes. If there is, returns the attributes in order.
Otherwise, returns None.
"""
l = [ ]
for attrs, d in image_attributes[tag].items():
remainder = [ i for i in attributes if i not in attrs ]
ca = getattr(d, "_choose_attributes", None)
if ca is not None:
chosen = ca(tag, remainder, None)
if chosen is not None:
l.append(list(attrs) + list(chosen))
else:
if not remainder:
l.append(attrs)
# Check to see if there's an image that is exactly the one we want.
for i in l:
if len(i) == len(attributes):
return tuple(i)
if len(l) != 1:
return None
return tuple(l[0])
def get_ordered_image_attributes(tag, attributes=(), sort=None):
"""
:doc: image_func
Returns a list of image tags, ordered in a way that makes sense to
present to the user.
`attributes`
If present, only attributes that are compatible with the given
attributes are considered. (Compatible means that the attributes
can be in a single image at the same time.)
`sort`
If not None, the returned list of attributes is sorted. This is a function
that should be used as a tiebreaker.
"""
sequences = [ ]
attrcount = collections.defaultdict(int)
attrtotalpos = collections.defaultdict(float)
for attrs, d in sorted(image_attributes[tag].items()):
la = getattr(d, "_list_attributes", None)
if la is not None:
sequence = list(attrs) + la(tag, [ i for i in attributes if i not in attrs ])
if not all(i in sequence for i in attributes):
continue
sequences.append(sequence)
else:
if not all(i in attrs for i in attributes):
continue
for i, attr in enumerate(attrs):
attrcount[attr] += 1
attrtotalpos[attr] += i
if sort is None:
return list(set(attrcount.keys()) | set(j for i in sequences for j in i))
# If we have a sequence, do a topological sort on the before-after relation -
# with an ajustment to make sure it will complete even if it loops.
rv = [ ]
# A map from an attribute to all the attributes it is after.
after = collections.defaultdict(set)
for i in sequences:
while i:
j = i.pop(0)
# Ensure it exists.
after[j]
for k in i:
after[k].add(j)
while after:
mincount = min(len(i) for i in after.values())
ready = set(k for k, v in after.items() if len(v) == mincount)
for i in ready:
del after[i]
for k in after:
after[k] = after[k] - ready
ready = list(ready)
ready.sort(key=lambda a : (sort(a), a))
rv.extend(ready)
l = [ ]
for attr in attrcount:
if attr not in rv:
l.append((attrtotalpos[attr] / attrcount[attr], sort(attr), attr))
l.sort()
for i in l:
rv.append(i[2])
return rv
def register_image(name, d):
"""
Registers the existence of an image with `name`, and that the image
used displayable d.
`name`
A tuple of strings.
"""
tag = name[0]
rest = name[1:]
images[name] = d
image_attributes[tag][rest] = d
image_names.append(" ".join(name))
def image_exists(name, exact=False):
"""
:doc: image_func
:name: renpy.has_image
Return true if an image with `name` exists, and false if no such image
exists.
`name`
Either a string giving an image name, or a tuple of strings giving
the name components.
`exact`
Returns true if and only if an image with the exact name exists -
parameterized matches are not included.
"""
if not isinstance(name, tuple):
name = tuple(name.split())
while name:
if name in images:
return True
if exact:
return False
name = name[:-1]
return False
def get_registered_image(name):
"""
:doc: image_func
If an image with the same name has been registered with renpy.register_image,
returns it. Otherwise, returns None.
"""
if not isinstance(name, tuple):
name = tuple(name.split())
return images.get(name)
def wrap_render(child, w, h, st, at):
rend = render(child, w, h, st, at)
rv = Render(rend.width, rend.height)
rv.blit(rend, (0, 0))
return rv
class ImageReference(renpy.display.core.Displayable):
"""
ImageReference objects are used to reference images by their name,
which is a tuple of strings corresponding to the name used to define
the image in an image statment.
"""
nosave = [ 'target' ]
target = None
old_transform = None
param_target = None
__version__ = 1
def after_upgrade(self, version):
if version < 1:
if isinstance(self.param_target, renpy.display.transform.Transform):
self.old_transform = self.param_target
def __init__(self, name, **properties):
"""
@param name: A tuple of strings, the name of the image. Or else
a displayable, containing the image directly.
"""
super(ImageReference, self).__init__(**properties)
self.name = name
def __unicode__(self):
return u"<ImageReference {!r}>".format(self.name)
def __hash__(self):
return hash(self.name)
def __eq__(self, o):
if self is o:
return True
if not self._equals(o):
return False
if self.name != o.name:
return False
return True
def _target(self):
if self.target is None:
self.find_target()
return self.target._target()
def find_target(self):
name = self.name
if isinstance(name, renpy.display.core.Displayable):
self.target = name
return True
if not isinstance(name, tuple):
name = tuple(name.split())
def error(msg):
self.target = renpy.text.text.Text(msg, color=(255, 0, 0, 255), xanchor=0, xpos=0, yanchor=0, ypos=0)
if renpy.config.debug:
raise Exception(msg)
args = [ ]
while name:
target = images.get(name, None)
if target is not None:
break
args.insert(0, name[-1])
name = name[:-1]
if not name:
error("Image '%s' not found." % ' '.join(self.name))
return False
if name and (self._args.name == name):
error("Image '{}' refers to itself.".format(' '.join(name)))
return False
args += self._args.args
try:
a = self._args.copy(name=name, args=args)
self.target = target._duplicate(a)
except Exception as e:
if renpy.config.debug:
raise
error(str(e))
# Copy the old transform over.
new_transform = self.target._target()
if isinstance(new_transform, renpy.display.transform.Transform):
if self.old_transform is not None:
new_transform.take_state(self.old_transform)
self.old_transform = new_transform
else:
self.old_transform = None
return True
_duplicatable = True
def _duplicate(self, args):
if args and args.args:
args.extraneous()
rv = self._copy(args)
rv.target = None
if isinstance(rv.name, renpy.display.core.Displayable):
if rv.name._duplicatable:
rv.name = rv.name._duplicate(args)
rv.find_target()
rv._duplicatable = rv.target._duplicatable
return rv
def _unique(self):
if self.target is None:
self.find_target()
self._duplicatable = self.target._duplicatable
def _in_current_store(self):
if self.target is None:
self.find_target()
target = self.target._in_current_store()
if target is self.target:
return self
rv = self._copy()
rv.target = target
return rv
def _handles_event(self, event):
if self.target is None:
return False
return self.target._handles_event(event)
def _hide(self, st, at, kind):
if self.target is None:
self.find_target()
return self.target._hide(st, at, kind)
def set_transform_event(self, event):
if self.target is None:
self.find_target()
return self.target.set_transform_event(event)
def event(self, ev, x, y, st):
if self.target is None:
self.find_target()
return self.target.event(ev, x, y, st)
def render(self, width, height, st, at):
if self.target is None:
self.find_target()
return wrap_render(self.target, width, height, st, at)
def get_placement(self):
if self.target is None:
self.find_target()
if not renpy.config.imagereference_respects_position:
return self.target.get_placement()
xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel = self.target.get_placement()
if xpos is None:
xpos = self.style.xpos
if ypos is None:
ypos = self.style.ypos
if xanchor is None:
xanchor = self.style.xanchor
if yanchor is None:
yanchor = self.style.yanchor
return xpos, ypos, xanchor, yanchor, xoffset, yoffset, subpixel
def visit(self):
if self.target is None:
self.find_target()
return [ self.target ]
class DynamicImage(renpy.display.core.Displayable):
"""
:doc: disp_imagelike
:args: (name)
A DynamicImage is a displayable that has text interpolation performed
on it to yield a string giving a new displayable. Such interpolation is
performed at the start of each interaction.
"""
nosave = [ 'raw_target' ]
# The target that this image currently resolves to.
target = None
# The raw target that the image resolves to, before it has been parameterized.
raw_target = None
# Have we been locked, so we never change?
locked = False
# The name used for hashing.
hash_name = None
def __init__(self, name, scope=None, **properties):
super(DynamicImage, self).__init__(**properties)
self.name = name
if isinstance(name, basestring) and ("[prefix_" in name):
self._duplicatable = True
if isinstance(name, list):
for i in name:
if ("[prefix_" in i):
self._duplicatable = True
if scope is not None:
self.find_target(scope)
self._uses_scope = True
else:
self._uses_scope = False
def _scope(self, scope, update):
return self.find_target(scope, update)
def __unicode__(self):
return u"DynamicImage {!r}".format(self.name)
def __hash__(self):
if self.hash_name is None:
if isinstance(self.name, list):
self.hash_name = tuple(self.name)
else:
self.hash_name = self.name
return hash(self.hash_name)
def __eq__(self, o):
if self is o:
return True
if not self._equals(o):
return False
if self.name != o.name:
return False
if self._uses_scope and (self.target != o.target):
return False
return True
def _target(self):
if self.target:
return self.target._target()
else:
return self
def set_style_prefix(self, prefix, root):
if (prefix != self.style.prefix) and self._duplicatable:
self.target = None
self.raw_target = None
super(DynamicImage, self).set_style_prefix(prefix, root)
def find_target(self, scope=None, update=True):
if self.locked and (self.target is not None):
return
if self._args.prefix is None:
if self._duplicatable:
prefix = self.style.prefix
else:
prefix = ""
else:
prefix = self._args.prefix
try:
search = [ ]
target = renpy.easy.dynamic_image(self.name, scope, prefix=prefix, search=search)
except KeyError as ke:
raise Exception("In DynamicImage %r: Could not find substitution '%s'." % (self.name, unicode(ke.args[0])))
except Exception as e:
raise Exception("In DynamicImage %r: %r" % (self.name, e))
if target is None:
error = "DynamicImage %r: could not find image." % (self.name,)
if len(search) == 1:
error += " (%r)" % (search[0],)
elif len(search) == 2:
error += " (%r, %r)" % (search[0], search[1])
elif len(search) > 2:
error += " (%r, %r, and %d more.)" % (search[0], search[1], len(search) - 2)
raise Exception(error)
if self.raw_target == target:
return False
if not update:
return True
self.raw_target = target
old_target = self.target
if target._duplicatable:
target = target._duplicate(self._args)
self.target = target
renpy.display.render.redraw(self, 0)
if not old_target:
return True
if not isinstance(old_target, renpy.display.motion.Transform):
return True
if not isinstance(target, renpy.display.motion.Transform):
self.target = target = renpy.display.motion.Transform(child=target)
target.take_state(old_target)
return True
def _duplicate(self, args):
if args and args.args:
args.extraneous()
rv = self._copy(args)
rv.target = None
rv.raw_target = None
# This does not set _duplicatable, since it should always remain the
# same.
return rv
def _in_current_store(self):
rv = self._copy()
if rv.target:
rv.target = rv.target._in_current_store()
rv.locked = True
return rv
def _handles_event(self, event):
if self.target is None:
return False
return self.target._handles_event(event)
def _hide(self, st, at, kind):
if self.target is None:
self.find_target()
return self.target._hide(st, at, kind)
def set_transform_event(self, event):
if self.target is None:
self.find_target()
return self.target.set_transform_event(event)
def event(self, ev, x, y, st):
if self.target is None:
self.find_target()
return self.target.event(ev, x, y, st)
def render(self, width, height, st, at):
if self.target is None:
self.find_target()
return wrap_render(self.target, width, height, st, at)
def get_placement(self):
if self.target is None:
self.find_target()
return self.target.get_placement()
def visit(self):
if self.target is None:
self.find_target()
return [ self.target ]
def per_interact(self):
old_target = self.target
if not self._uses_scope:
self.find_target()
if old_target is not self.target:
self.target.visit_all(lambda i : i.per_interact())
class ShownImageInfo(renpy.object.Object):
"""
This class keeps track of which images are being shown right now,
and what the attributes of those images are. (It's used for a similar
purpose during prediction, regarding the state in the future.)
"""
__version__ = 2
def __init__(self, old=None):
"""
Creates a new object. If `old` is given, copies the default state
from old, otherwise initializes the object to a default state.
"""
if old is None:
# A map from (layer, tag) -> tuple of attributes
# This doesn't necessarily correspond to something that is
# currently showing, as we can remember the state of a tag
# for use in SideImage.
self.attributes = { }
# A set of (layer, tag) pairs that are being shown on the
# screen right now.
self.shown = set()
else:
self.attributes = old.attributes.copy()
self.shown = old.shown.copy()
def after_upgrade(self, version):
if version < 2:
self.attributes = { }
self.shown = set()
for layer in self.images:
for tag in self.images[layer]:
self.attributes[layer, tag] = self.images[layer][tag][1:]
self.shown.add((layer, tag))
def get_attributes(self, layer, tag, default=()):
"""
Get the attributes associated the image with tag on the given
layer.
"""
if layer is None:
layer = renpy.config.tag_layer.get(tag, "master")
return self.attributes.get((layer, tag), default)
def showing(self, layer, name, exact=False):
"""
Returns true if name is the prefix of an image that is showing
on layer, or false otherwise.
"""
tag = name[0]
rest = name[1:]
if layer is None:
layer = renpy.config.tag_layer.get(tag, "master")
if (layer, tag) not in self.shown:
return None
shown = self.attributes[layer, tag]
if len(shown) < len(rest):
return False
if exact and (len(shown) != len(rest)):
return False
for a, b in zip(shown, rest):
if a != b:
return False
return True
def get_showing_tags(self, layer):
"""
Returns the set of tags being shown on `layer`.
"""
return { t for l, t in self.shown if l == layer }
def get_hidden_tags(self, layer):
"""
Returns the set of tags on layer that have attributes,
but aren't being shown.
"""
return { t for l, t in self.attributes if l == layer if (l, t) not in self.shown }
def predict_scene(self, layer):
"""
Predicts the scene statement being called on layer.
"""
if layer is None:
layer = 'master'
for l, t in self.attributes.keys():
if l == layer:
del self.attributes[l, t]
self.shown = set((l, t) for l, t in self.shown if l != layer)
def predict_show(self, layer, name, show=True):
"""
Predicts name being shown on layer.
`show`
If True, the image will be flagged as being shown to the user. If
False, only the attributes will be updated.
"""
tag = name[0]
rest = name[1:]
if layer is None:
layer = renpy.config.tag_layer.get(tag, "master")
self.attributes[layer, tag] = rest
if show:
self.shown.add((layer, tag))
def predict_hide(self, layer, name):
tag = name[0]
if layer is None:
layer = renpy.config.tag_layer.get(tag, "master")
if (layer, tag) in self.attributes:
del self.attributes[layer, tag]
self.shown.discard((layer, tag))
def apply_attributes(self, layer, tag, name, wanted=[], remove=[]):
"""
Given a layer, tag, and an image name (with attributes),
returns the canonical name of an image, if one exists. Raises
an exception if it's ambiguious, and returns None if an image
with that name couldn't be found.
"""
if layer is None:
layer = renpy.config.tag_layer.get(tag, "master")
# If the name matches one that exactly exists, return it.
if (name in images) and not (wanted or remove):
ca = getattr(images[name], "_choose_attributes", None)
if ca is None:
return name
nametag = name[0]
# The set of attributes a matching image may have.
optional = set(wanted) | set(self.attributes.get((layer, tag), [ ]))
# The set of attributes a matching image must have/not have.
# Evaluated in order.
required = set()
for i in name[1:]:
if i[0] == "-":
optional.discard(i[1:])
required.discard(i[1:])
else:
required.add(i)
for i in remove:
optional.discard(i)
return self.choose_image(nametag, required, optional, name)
def choose_image(self, tag, required, optional, exception_name):
# The longest length of an image that matches.
max_len = -1
# The list of matching images.
matches = None
for attrs, d in image_attributes[tag].items():
ca = getattr(d, "_choose_attributes", None)
if ca:
ca_required = { i for i in required if i not in attrs }
ca_optional = { i for i in optional if i not in attrs }
newattrs = ca(tag, ca_required, ca_optional)
if newattrs is None:
continue
attrs = attrs + newattrs
num_required = 0
for i in attrs:
if i in required:
num_required += 1
continue
elif i not in optional:
break
else:
# We don't have any not-found attributes. But we might not
# have all of the attributes.
if num_required != len(required):
continue
len_attrs = len(set(attrs))
if len_attrs < max_len:
continue
if len_attrs > max_len:
max_len = len_attrs
matches = [ ]
matches.append((tag, ) + attrs)
if matches is None:
return None
if len(matches) == 1:
return matches[0]
if exception_name:
raise Exception("Showing '" + " ".join(exception_name) + "' is ambiguous, possible images include: " + ", ".join(" ".join(i) for i in matches))
else:
return None
renpy.display.core.ImagePredictInfo = ShownImageInfo
# Functions that have moved from this module to other modules,
# that live here for the purpose of backward-compatibility.
Image = renpy.display.im.image
Solid = renpy.display.imagelike.Solid
Frame = renpy.display.imagelike.Frame
ImageButton = renpy.display.behavior.ImageButton