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

502 lines
12 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.
from __future__ import print_function
import os
import copy
import time
import renpy
from renpy.loadsave import dump, dumps, loads
# The class that's used to hold the persistent data.
class Persistent(object):
def __init__(self):
self._update()
def __setstate__(self, data):
self.__dict__.update(data)
def __getstate__(self):
return self.__dict__
# Undefined attributes return None.
def __getattr__(self, attr):
if attr.startswith("__") and attr.endswith("__"):
raise AttributeError("Persistent object has no attribute %r", attr)
return None
def _clear(self, progress=False):
"""
Resets the persistent data.
`progress`
If true, also resets progress data that Ren'Py keeps.
"""
keys = list(self.__dict__)
for i in keys:
if i[0] == "_":
continue
del self.__dict__[i]
if progress:
self._seen_ever.clear()
self._seen_images.clear()
self._chosen.clear()
self._seen_audio.clear()
def _update(self):
"""
Updates the persistent data to be the latest version of
the persistent data.
"""
if self._preferences is None:
self._preferences = renpy.preferences.Preferences()
# Initialize the set of statements seen ever.
if not self._seen_ever:
self._seen_ever = { }
# Initialize the set of images seen ever.
if not self._seen_images:
self._seen_images = { }
# Initialize the set of chosen menu choices.
if not self._chosen:
self._chosen = { }
if not self._seen_audio:
self._seen_audio = { }
# The set of seen translate identifiers.
if not self._seen_translates:
self._seen_translates = set()
# A map from the name of a field to the time that field was last
# changed at.
if self._changed is None:
self._changed = {
"_preferences" : 0,
"_seen_ever" : 0,
"_chosen" : 0,
"_seen_audio" : 0,
"_seen_translates" : 0,
}
renpy.game.Persistent = Persistent
renpy.game.persistent = Persistent()
def safe_deepcopy(o):
"""
A "safe" version of deepcopy. If an object doesn't implement __eq__
correctly, we replace it with its original.
This tries to ensure we don't constantly find changes in the same
field.
"""
rv = copy.deepcopy(o)
if not (o == rv):
if renpy.config.developer:
raise Exception("To be persisted, %r must support equality comparison." % o)
else:
rv = o
return rv
# A map from field names to a backup of the field names in the persistent
# object.
backup = { }
def find_changes():
"""
This finds changes in the persistent object. When it finds a change, it
backs up that changed, and puts the current time for that field into
persistent._changed.
This returns True if there was at least one change, and False
otherwise.
"""
rv = False
now = time.time()
persistent = renpy.game.persistent
pvars = vars(persistent)
fields = set(backup.keys()) | set(pvars.keys())
for f in fields:
if f == "_changed":
continue
old = backup.get(f, None)
new = pvars.get(f, None)
if not (new == old):
persistent._changed[f] = now
backup[f] = safe_deepcopy(new)
rv = True
return rv
def load(filename):
"""
Loads persistence data from `filename`. Returns None if the data
could not be loaded, or a Persistent object if it could be
loaded.
"""
if not os.path.exists(filename):
return None
# Unserialize the persistent data.
try:
f = file(filename, "rb")
s = f.read().decode("zlib")
f.close()
persistent = loads(s)
except:
import renpy.display
try:
renpy.display.log.write("Loading persistent.")
renpy.display.log.exception()
except:
pass
return None
persistent._update()
return persistent
def init():
"""
Loads the persistent data from disk.
This performs the initial load of persistent data from the local
disk, so that we can configure the savelocation system.
"""
filename = os.path.join(renpy.config.savedir, "persistent.new")
persistent = load(filename)
if persistent is None:
filename = os.path.join(renpy.config.savedir, "persistent")
persistent = load(filename)
if persistent is None:
persistent = Persistent()
# Create the backup of the persistent data.
v = vars(persistent)
for k, v in vars(persistent).iteritems():
backup[k] = safe_deepcopy(v)
return persistent
# A map from field name to merge function.
registry = { }
def register_persistent(field, func):
"""
:doc: persistent
Registers a function that is used to merge values of a persistent field
loaded from disk with values of current persistent object.
`field`
The name of a field on the persistent object.
`function`
A function that is called with three parameters, `old`, `new`, and
`current`:
`old`
The value of the field in the older object.
`new`
The value of the field in the newer object.
`current`
The value of the field in the current persistent object. This is
provided for cases where the identity of the object referred to
by the field can't change.
The function is expected to return the new value of the field in the
persistent object.
"""
registry[field] = func
def default_merge(old, new, current):
return new
def dictset_merge(old, new, current):
current.update(old)
current.update(new)
return current
register_persistent("_seen_ever", dictset_merge)
register_persistent("_seen_images", dictset_merge)
register_persistent("_seen_audio", dictset_merge)
register_persistent("_chosen", dictset_merge)
def merge(other):
"""
Merges `other` (which must be a persistent object) into the
current persistent object.
"""
now = time.time()
persistent = renpy.game.persistent
pvars = vars(persistent)
ovars = vars(other)
fields = set(pvars.keys()) | set(ovars.keys())
for f in fields:
pval = pvars.get(f, None)
oval = ovars.get(f, None)
if pval == oval:
continue
ptime = persistent._changed.get(f, 0)
otime = other._changed.get(f, 0)
otime = min(now, otime)
if ptime >= otime:
new = pval
old = oval
t = ptime
else:
new = oval
old = pval
t = otime
merge_func = registry.get(f, default_merge)
val = merge_func(old, new, pval)
pvars[f] = val
backup[f] = safe_deepcopy(val)
persistent._changed[f] = t
# The mtime of the most recently processed savefile.
persistent_mtime = None
def check_update():
"""
Checks to see if we need to run update. If we do, runs update and
restarts the interaction.
"""
for mtime, _data in renpy.loadsave.location.load_persistent():
if mtime > persistent_mtime:
break
else:
return
update()
renpy.exports.restart_interaction()
def update(force_save=False):
"""
Loads the persistent data from persistent files that are newer than
persistent_mtime, and merges it into the persistent object.
"""
need_save = find_changes()
need_save = need_save or force_save
global persistent_mtime
# A list of (mtime, other) pairs, where other is a persistent file
# we might want to merge in.
pairs = renpy.loadsave.location.load_persistent()
pairs.sort()
# Deals with the case where we don't have any persistent data for
# some reason.
mtime = persistent_mtime
for mtime, other in pairs:
if mtime <= persistent_mtime:
continue
if other is None:
continue
merge(other)
persistent_mtime = mtime
if need_save:
save()
should_save_persistent = True
def save():
"""
Saves the persistent data to disk.
"""
if not should_save_persistent:
return
try:
data = dumps(renpy.game.persistent).encode("zlib")
renpy.loadsave.location.save_persistent(data)
except:
if renpy.config.developer:
raise
################################################################################
# MultiPersistent
################################################################################
class _MultiPersistent(object):
def __getstate__(self):
state = self.__dict__.copy()
del state['_filename']
return state
def __setstate__(self, state):
self.__dict__.update(state)
def __getattr__(self, name):
if name.startswith("__") and name.endswith("__"):
raise AttributeError()
return None
def save(self):
fn = self._filename
f = file(fn + ".new", "wb")
dump(self, f)
f.close()
try:
os.rename(fn + ".new", fn)
except:
os.unlink(fn)
os.rename(fn + ".new", fn)
def MultiPersistent(name):
name = renpy.exports.fsencode(name)
if not renpy.game.context().init_phase:
raise Exception("MultiPersistent objects must be created during the init phase.")
if renpy.android:
files = [ os.path.join(os.environ['ANDROID_OLD_PUBLIC'], '../RenPy/Persistent') ]
elif renpy.ios:
raise Exception("MultiPersistent is not supported on iOS.")
elif renpy.windows:
files = [ os.path.expanduser("~/RenPy/Persistent") ]
if 'APPDATA' in os.environ:
files.append(os.environ['APPDATA'] + "/RenPy/persistent")
elif renpy.macintosh:
files = [ os.path.expanduser("~/.renpy/persistent"),
os.path.expanduser("~/Library/RenPy/persistent") ]
else:
files = [ os.path.expanduser("~/.renpy/persistent") ]
if "RENPY_MULTIPERSISTENT" in os.environ:
files = [ os.environ["RENPY_MULTIPERSISTENT"] ]
# Make the new persistent directory, why not?
try:
os.makedirs(files[-1])
except:
pass
fn = "" # prevent a warning from happening.
# Find the first file that actually exists. Otherwise, use the last
# file.
for fn in files:
fn = fn + "/" + name
if os.path.exists(fn):
break
try:
rv = loads(file(fn, "rb").read())
except:
rv = _MultiPersistent()
rv._filename = fn # W0201
return rv
renpy.loadsave._MultiPersistent = _MultiPersistent
renpy.loadsave.MultiPersistent = MultiPersistent