Wardrobe performance improvements and bug fixes

* Implemented new DollThread method with thread-safe locking mechanism and pickling support for thread event queues
* Added memoization for wardrobe elements
* Added threading for various wardrobe-related methods
* Added lazyloading (to avoid render stalls)
* Added button generation for DollCloth and DollOutfit instances
* Significantly reduced code repetition inside the wardrobe loop
* Added new methods for the Doll, and improved others.
* Fixed viewport adjustment values resetting on interaction
* Fixed character chit-chats performance issues
* Updated saves compatibility patch
This commit is contained in:
LoafyLemon 2023-07-11 22:57:49 +01:00
parent a2794e3e47
commit 4c98cbe669
9 changed files with 457 additions and 257 deletions

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,7 @@ init -1 python:
class DollMethods(SlottedObject): class DollMethods(SlottedObject):
"""Container class for commonly used methods and attributes""" """Container class for commonly used methods and attributes"""
_loading = Text("Loading", align=(0.5, 0.5))
_image = Null() _image = Null()
_image_cached = False _image_cached = False
blacklist_toggles = ("hair", "glasses", "pubes", "piercing", "makeup", "tattoo", "earrings") blacklist_toggles = ("hair", "glasses", "pubes", "piercing", "makeup", "tattoo", "earrings")

View File

@ -265,9 +265,9 @@ init python:
"""Takes argument containing string cloth type. Returns equipped object for cloth type.""" """Takes argument containing string cloth type. Returns equipped object for cloth type."""
return self.states[slot][0] return self.states[slot][0]
def get_equipped_item(self, items): def get_equipped_wardrobe_item(self, items, subcat):
"""Returns first equipped item from a list or None.""" """Returns first equipped item from a list or None."""
for i in items: for i in items.get(subcat):
if self.is_equipped_item(i): if self.is_equipped_item(i):
return i return i
return None return None
@ -391,10 +391,17 @@ init python:
def is_equipped_item(self, item): def is_equipped_item(self, item):
"""Takes DollCloth object or list of objects. Returns True if item is equipped, False otherwise.""" """Takes DollCloth object or list of objects. Returns True if item is equipped, False otherwise."""
if item.is_multislot():
return bool(next((k for k, v in self.states.items() if v[0] == item), False))
return self.get_equipped(item.type) == item if isinstance(item, DollCloth):
if item.is_multislot():
return bool(next((k for k, v in self.states.items() if v[0] == item), False))
return self.get_equipped(item.type) == item
elif isinstance(item, DollOutfit):
compare_object = self.create_outfit(temp=True)
# current_item = next( (x for x in char_active.outfits if _outfit == x), None)
return item == compare_object
def is_worn(self, *args): def is_worn(self, *args):
"""Takes argument(s) containing string cloth type(s). Returns True if worn, False otherwise.""" """Takes argument(s) containing string cloth type(s). Returns True if worn, False otherwise."""
@ -592,3 +599,11 @@ init python:
self.emote = emote self.emote = emote
self.is_stale() self.is_stale()
def clear_outfit_button_cache(self):
DollThread.stop_all()
for i in self.outfits:
i._button.last_item = i._loading
i.clear_button_cache()

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,92 @@
init python: init python early:
import threading import threading
import queue
class DollThread(threading.Thread, NoRollback): class DollThread(threading.Thread, NoRollback):
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): __lock = threading.RLock()
_count = 0
_instances = _list()
_interval = 0.05 if renpy.android else 0.025
def __init__(self, interval=None, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None):
threading.Thread.__init__(self, group, target, name, args, kwargs, daemon=daemon) threading.Thread.__init__(self, group, target, name, args, kwargs, daemon=daemon)
self._return = None self._return = None
self.interval = interval or self._interval
self._delay = threading.Timer(self.interval, self._execute)
self._stop = threading.Event()
def start(self):
DollThread._instances.append(self)
DollThread._count += 1
super().start()
def run(self): def run(self):
if self._target is not None: with DollThread.__lock:
self._return = self._target(*self._args, **self._kwargs) self._delay.start()
self._delay.join()
self.stop()
renpy.restart_interaction() def _execute(self):
while not self._stop.is_set():
if self._target is not None:
self._return = self._target(*self._args, **self._kwargs)
renpy.restart_interaction()
break
def join(self, timeout=1): def join(self, timeout=None):
threading.Thread.join(self, timeout=timeout) super().join(timeout=timeout)
return self._return return self._return
def stop(self):
self._stop.set()
DollThread._instances.remove(self)
DollThread._count -= 1
@classmethod
def stop_all(cls):
with cls.__lock:
for thread in cls._instances:
thread.stop()
# Allow threads to exit gracefully before forceful termination
for thread in cls._instances:
thread.join()
class DefaultQueue(queue.Queue, NoRollback):
def __init__(self):
super().__init__()
self.last_item = None
def put(self, item, block=True, timeout=None):
super().put(item, block, timeout)
self.last_item = item
def get_with_default(self, default):
try:
return self.get(block=False)
except queue.Empty:
return self.last_item or default
def __getstate__(self):
state = self.__dict__.copy()
del state['last_item']
del state['mutex']
del state['not_empty']
del state['not_full']
del state['all_tasks_done']
return state
def __setstate__(self, state):
self.__dict__.update(state)
self.last_item = None
self.mutex = threading.Lock()
self.not_empty = threading.Condition(self.mutex)
self.not_full = threading.Condition(self.mutex)
self.all_tasks_done = threading.Condition(self.mutex)
def __reduce__(self):
return (DefaultQueue, ())
def __reduce_ex__(self, protocol):
return DefaultQueue, (), self.__getstate__(), None, iter([])

View File

@ -101,11 +101,19 @@ init python:
if current < 1.452: if current < 1.452:
# Fix makeup object types inside saved outfits
for i in states.dolls: for i in states.dolls:
doll = getattr(store, i) doll = getattr(store, i)
for j in doll.wardrobe_list:
# Add new button handler for clothes
j._button = DefaultQueue()
for j in doll.outfits: for j in doll.outfits:
# Add new button handler for outfits
j._button = DefaultQueue()
# Fix makeup object types inside saved outfits
if j.has_type("makeup"): if j.has_type("makeup"):
objects = [x.parent.clone() for x in j.group] objects = [x.parent.clone() for x in j.group]

View File

@ -173,9 +173,27 @@ init -1 python:
return return
def list_outfit_files(): def list_outfit_files():
path = "{}/outfits/".format(config.gamedir)
@functools.cache
def build_button(rp):
style = "wardrobe_button"
child = Fixed(Transform(rp, xsize=96, fit="contain", yalign=1.0, yoffset=-6), Frame(gui.format("interface/frames/{}/iconframe.webp"), 6, 6), xysize=(96, 168))
action = Return(["import", rp])
hover_foreground = "#ffffff80"
return Button(child=child, action=action, hover_foreground=hover_foreground, style=style)
path = f"{config.gamedir}/outfits/"
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
return [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and f.endswith(".png")] files = []
for f in os.listdir(path):
fp = os.path.join(path, f)
rp = os.path.relpath(fp, config.gamedir)
if os.path.isfile(os.path.join(path, f)) and f.endswith(".png"):
files.append(build_button(rp))
return files

View File

@ -68,10 +68,6 @@ init python:
if wardrobe_chitchats: if wardrobe_chitchats:
_skipping = True _skipping = True
renpy.suspend_rollback(False) renpy.suspend_rollback(False)
renpy.hide_screen("wardrobe")
renpy.hide_screen("wardrobe_menuitem")
renpy.hide_screen("wardrobe_outfit_menuitem")
renpy.show("gui_fade", zorder=10, behind=get_character_tag(states.active_girl))
renpy.block_rollback() renpy.block_rollback()
renpy.call(get_character_response(states.active_girl, what), arg) renpy.call_in_new_context(get_character_response(states.active_girl, what), arg)
return return

File diff suppressed because it is too large Load Diff