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):
"""Container class for commonly used methods and attributes"""
_loading = Text("Loading", align=(0.5, 0.5))
_image = Null()
_image_cached = False
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."""
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."""
for i in items:
for i in items.get(subcat):
if self.is_equipped_item(i):
return i
return None
@ -391,10 +391,17 @@ init python:
def is_equipped_item(self, item):
"""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):
"""Takes argument(s) containing string cloth type(s). Returns True if worn, False otherwise."""
@ -592,3 +599,11 @@ init python:
self.emote = emote
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 queue
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)
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):
if self._target is not None:
self._return = self._target(*self._args, **self._kwargs)
with DollThread.__lock:
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):
threading.Thread.join(self, timeout=timeout)
def join(self, timeout=None):
super().join(timeout=timeout)
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:
# Fix makeup object types inside saved outfits
for i in states.dolls:
doll = getattr(store, i)
for j in doll.wardrobe_list:
# Add new button handler for clothes
j._button = DefaultQueue()
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"):
objects = [x.parent.clone() for x in j.group]

View File

@ -173,9 +173,27 @@ init -1 python:
return
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):
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:
_skipping = True
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.call(get_character_response(states.active_girl, what), arg)
renpy.call_in_new_context(get_character_response(states.active_girl, what), arg)
return

File diff suppressed because it is too large Load Diff