init python: class Doll(DollMethods): # 0 - 50 = Skin/Body Layers # 51 - 100 = Face Layers # 101 - 300+ = Clothes Layers body_layers = { "frame": 0, "legs": 1, "hips": 2, "abdomen": 3, "chest": 4, "arms": 5, "head": 6, } face_layers = { "tears": 75, "eyebrows": 70, "pupils": 65, "eyes": 60, "mouth": 55, "cheeks": 51 } clothing_layers = { "makeup": 111, # multislot "accessory": 121, # multislot "piercing": 131, # multislot "tattoo": 141, # multislot "pubes": 151, "stockings": 171, "panties": 161, "garterbelt": 181, "bottom": 191, "bra": 201, "top": 211, "gloves": 221, "robe": 231, "neckwear": 241, "hair": 251, "earrings": 261, "glasses": 271, "headgear": 281 } __slots__ = ("wardrobe", "wardrobe_list", "blacklist", "outfits", "name", "states", "face", "body", "cum", \ "pose", "emote", "_hash", "zorder", "layer", "animation", "tag", "pos", "zoom", "xzoom", "align", "modpath") def __init__(self, name, modpath=None): self.wardrobe = {} self.wardrobe_list = [] self.blacklist = [] self.outfits = [] self.name = name self.states = {k: [None, v, True] for k, v in (self.clothing_layers | self.body_layers).items()} self.face = DollFace(self) self.body = DollBody(self) self.cum = DollCum(self) self.pose = "default" self.emote = Null() self._hash = None self._sprite = DefaultQueue() # Image properties self.zorder = 15 self.layer = "dolls" self.animation = None self.tag = f"{name}_main" # Transform properties self.pos = (0, 0) self.zoom = 0.5 self.xzoom = 1 self.modpath = "mods/" + posixpath.normpath(modpath) if modpath else "" self.build_image() # Add doll name to global doll states store try: renpy.store.states.dolls.add(name) except AttributeError: renpy.store.states.dolls = {name} # TODO: Experiment with cache for full dolls, maybe for android? def is_stale(self): curr_hash = self.generate_hash() # if (stale := curr_hash != self._hash): # self.remove_disk_cache(self._hash) self._hash = curr_hash return stale def generate_hash(self): clothes_hash = str([x[0]._hash for x in self.states.values() if istype(x[0], (DollCloth, DollClothDynamic, DollMakeup)) and x[2]]) salt = str( [self.name, self.pose, str(self.body._hash), str(self.face._hash), str(self.cum._hash), clothes_hash] ) return hashstring(salt) def show(self, force=False, ignore_skipping=False): if renpy.get_screen(("wardrobe", "animatedCG", "studio")) or renpy.showing("cg"): return if renpy.is_skipping() and not ignore_skipping: return if not force and not renpy.showing(get_character_tag(self.name), layer=self.layer): return base_transform = doll_transform(self.pos, self.zoom, self.xzoom) animation = self.animation at_list = [base_transform] if animation: at_list.append(animation) renpy.show(name=self.tag, at_list=at_list, layer=self.layer, what=self.image, zorder=self.zorder) def hide(self): renpy.hide(name=self.tag, layer=self.layer) @functools.cache def _build_image(self, hash): from itertools import chain # Note: Bodyparts are a part of 'self.states' now. sprites = list(chain.from_iterable( (self.face.build_image(self.face._hash), self.cum.build_image(self.cum._hash), *(x[0].build_image(x[0]._hash) for x in self.states.values() if x[0] and x[2])) )) masks = [sprites.pop(sprites.index(x)) for x in sprites if x[0] == "mask"] sprites.sort(key=itemgetter(2)) masks.sort(key=itemgetter(2)) back_sprites = [x[1] for x in sprites if x[2] < 0] #Apply alpha mask for m in masks: _, mask, mask_zorder = m for i, s in enumerate(sprites): _, sprite, sprite_zorder = s if i < 1 or mask_zorder > sprite_zorder: continue masked = AlphaMask(Fixed(*(x[1] for x in sprites[:i]), fit_first=True), mask) sprites = sprites[i:] sprites.insert(0, (None, masked, mask_zorder)) break sprites = back_sprites + [x[1] for x in sprites] return Fixed(*sprites, self.emote, fit_first=True) def build_image(self): def _func(self, hash): result = self._build_image(hash) self._sprite.put(result) thread = DollThread(target=_func, args=(self, self._hash)) thread.start() def _image(self, st, at): return self._sprite.get_with_default(Null()), None def is_stale(self): curr_hash = self.generate_hash() stale = curr_hash != self._hash self._hash = curr_hash if stale and settings.get("multithreading"): self.build_image() return stale @property def image(self): if not renpy.is_skipping() and self.is_stale(): self.show() if settings.get("multithreading"): return DynamicDisplayable(self._image) else: return self._build_image(self._hash) def equip(self, obj, remove_old=True): """Takes DollCloth or DollOutfit object to equip.""" def _equip_item(item, color=None): if item.is_multislot(): for i in range(100): multislot = item.type + str(i) if multislot not in self.states or self.states[multislot][0] is None: zorder = self.states[item.type][1] self.states[multislot] = [item, zorder, True] break else: zorder = self.states[item.type][1] self.states[item.type] = [item, zorder, True] if self.is_blacklisted(item.type): self.unequip(*self.get_blacklister(item.type)) if item.blacklist: self.unequip(*item.blacklist) for tracking in self.get_trackers_list(item.type): tracking.is_stale() if color: item.set_color(color) item.is_stale() def _equip_bodypart(item): _equip_item(item) self.body.is_stale() if istype(obj, (DollCloth, DollClothDynamic, DollMakeup)): _equip_item(obj) elif istype(obj, DollBodypart): _equip_bodypart(obj) elif isinstance(obj, DollOutfit): if remove_old: self.unequip("clothes", "makeup") for item in obj.group: _equip_item(item.parent, color=item.color) elif isinstance(obj, (list, tuple)): for item in obj: _equip_item(item) self.rebuild_blacklist() update_chibi(self.name) self.cum.is_stale() self.is_stale() self.show() def unequip(self, *args): """Takes argument(s) containing string cloth type(s) to unequip.""" def _tracker_rebuild(type): for tracking in self.get_trackers_list(type): tracking.is_stale() def _unequip_all(): for k, v in self.states.items(): if not k in self.blacklist_unequip: v[0], v[2] = None, True _tracker_rebuild(k) def _unequip_type(type): for k, v in self.states.items(): if not k in self.blacklist_unequip and istype(v[0], type): v[0], v[2] = None, True _tracker_rebuild(k) def _unequip_slot(slot): if slot in self.blacklist_unequip: return if slot in self.multislots: for k, v in self.states.items(): if v[0] and v[0].type == slot: v[0], v[2] = None, True else: self.states[slot][0], self.states[slot][2] = None, True _tracker_rebuild(slot) for arg in args: if isinstance(arg, str): if arg == "all": _unequip_all() elif arg == "clothes": _unequip_type((DollCloth, DollClothDynamic)) elif arg == "bodyparts": _unequip_type(DollBodypart) elif arg == "makeup": _unequip_type(DollMakeup) else: _unequip_slot(arg) elif isinstance(arg, DollCloth): if arg.is_multislot(): slot = next((k for k, v in self.states.items() if v[0] == arg), None) if not slot: continue _unequip_slot(slot) else: _unequip_slot(arg.type) elif isinstance(arg, DollOutfit): for item in arg.group: _unequip_slot(item.type) self.rebuild_blacklist() update_chibi(self.name) self.cum.is_stale() self.is_stale() self.show() def get_equipped(self, slot): """Takes argument containing string cloth type. Returns equipped object for cloth type.""" return self.states[slot][0] def get_equipped_wardrobe_item(self, items, subcat): """Returns first equipped item from a list or None.""" for i in items.get(subcat, []): if self.is_equipped_item(i): return i return None def strip(self, *args): """Takes argument(s) containing string cloth type(s) to temporarily displace (hide).""" def _tracker_rebuild(type): for tracking in self.get_trackers_list(type): tracking.is_stale() def _strip_all(): for k, v in self.states.items(): if not k.startswith(self.blacklist_unequip): v[2] = False _tracker_rebuild(k) def _strip_type(type): for k, v in self.states.items(): if istype(v[0], type) and not k in self.blacklist_unequip and (k in self.multislots or not k.startswith(self.blacklist_strip)): v[2] = False _tracker_rebuild(k) def _strip_slot(slot): if slot in self.blacklist_unequip: return if slot in self.multislots: for k, v in self.states.items(): if k.startswith(slot): v[2] = False else: self.states[slot][2] = False _tracker_rebuild(slot) for arg in args: if arg == "all": _strip_all() elif arg == "clothes": _strip_type((DollCloth, DollClothDynamic)) elif arg == "makeup": _strip_type(DollMakeup) elif arg == "bodyparts": _strip_type(DollBodypart) else: _strip_slot(arg) update_chibi(self.name) self.is_stale() self.show() def wear(self, *args): """Takes argument(s) containing string cloth type(s) to temporarily displace (hide).""" def _tracker_rebuild(type): for tracking in self.get_trackers_list(type): tracking.is_stale() def _wear_all(): for k, v in self.states.items(): v[2] = True _tracker_rebuild(k) def _wear_type(type): for k, v in self.states.items(): if istype(v[0], type): v[2] = True _tracker_rebuild(v) def _wear_slot(slot): if slot in self.multislots: for k, v in self.states.items(): if k.startswith(slot): v[2] = True else: self.states[slot][2] = True _tracker_rebuild(slot) for arg in args: if arg == "all": _wear_all() elif arg == "clothes": _wear_type((DollCloth, DollClothDynamic)) elif arg == "makeup": _wear_type(DollMakeup) elif arg == "bodyparts": _wear_type(DollBodypart) else: _wear_slot(arg) update_chibi(self.name) self.is_stale() self.show() def is_equipped(self, *args): """Takes argument containing string cloth type. Returns True if slot is occupied, False otherwise.""" for arg in args: if arg in self.multislots: return any(bool(v[0]) for k, v in self.states.items() if k.startswith(arg)) else: if not self.states[arg][0]: return False return True def is_any_equipped(self, *args): """Takes arguments containing string cloth types. Returns True if ANY of them is equipped, False otherwise.""" def _is_equipped_type(type): for k, v in self.states.items(): if not k in self.blacklist_toggles and istype(v[0], type): if self.is_equipped(k): return True return False state = False for arg in args: if arg == "clothes": state = _is_equipped_type((DollCloth, DollClothDynamic)) elif arg == "makeup": state = _is_equipped_type(DollMakeup) elif arg == "bodyparts": state = _is_equipped_type(DollBodypart) else: state = self.is_equipped(arg) if state is True: break return state def is_equipped_item(self, item): """Takes DollCloth object or list of objects. Returns True if item is equipped, False otherwise.""" 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.""" for arg in args: if arg in self.multislots: return any( (v[0] and v[2]) for k, v in self.states.items() if k.startswith(arg)) else: if not self.states[arg][0] or not self.states[arg][2]: return False return True def is_any_worn(self, *args): """Takes arguments containing string cloth types. Returns True if ANY of them is worn, False otherwise.""" def _is_worn_type(type): for k, v in self.states.items(): if not k in self.blacklist_toggles and istype(v[0], type): if self.is_worn(k): return True return False state = False for arg in args: if arg == "clothes": state = _is_worn_type((DollCloth, DollClothDynamic)) elif arg == "makeup": state = _is_worn_type(DollMakeup) elif arg == "bodyparts": state = _is_worn_type(DollBodypart) else: state = self.is_worn(arg) if state is True: break return state def set_face(self, **kwargs): self.face.set_face(**kwargs) for i in self.states.values(): if istype(i[0], DollMakeup): i[0].is_stale() self.cum.is_stale() def get_face(self): """Returns a dictionary containing currently set facial expressions. Used in character studio.""" return self.face._face.copy() def set_body_hue(self, arg): """Takes integer between 0 - 359, rotates the character body colour by given amount.""" self.set_body_matrix(HueMatrix(arg)) def set_body_matrix(self, arg): self.body.set_matrix(arg) for i in self.states.values(): if i[0]: i[0].is_stale() self.is_stale() self.show() def set_cum(self, *args, **kwargs): """Takes keyword argument(s) containing string name(s) of cum layers to apply or None.""" self.cum.set_cum(*args, **kwargs) self.show() def set_pose(self, pose): pose = "default" if pose is None else pose self.pose = pose self.body.is_stale() self.face.is_stale() self.cum.is_stale() for i in self.states.values(): if i[0]: i[0].is_stale() self.is_stale() self.show() def rebuild_blacklist(self): blacklist = [] for v in self.states.values(): if v[0]: blacklist.extend(v[0].blacklist) self.blacklist = list(set(blacklist)) def is_blacklisted(self, type): """Takes string cloth type. Returns True if cloth type is blacklisted.""" return True if type in self.blacklist else False def get_blacklister(self, type): """Takes string cloth type. Returns a list of clothing types that report incompatibility.""" return [x[0].type for x in self.states.values() if x[0] and type in x[0].blacklist] def get_trackers_list(self, type): """Takes string cloth type. Returns a list of clothing types that report incompatibility.""" return [x[0] for x in self.states.values() if istype(x[0], DollClothDynamic) and type == x[0].tracking] def create_outfit(self, temp=False): """Creates a copy of the current character clothes and stores it. Used only for comparing instances inside the wardrobe.""" return DollOutfit([x[0] for x in self.states.values() if x[0] and x[0].type not in self.body_layers], True, temp=temp) def import_outfit(self, path, fromfile=True): """Imports outfit from .png file or clipboard text.""" # Grab data if fromfile: try: imported = image_payload.extract(path) except Exception as e: renpy.notify("Import failed: Corrupted file.") print(e) renpy.block_rollback() return None else: imported = get_clipboard() # Evaluate data if imported: try: imported = make_revertable(evaluate(imported)) except Exception as e: renpy.notify("Import failed: Corrupted outfit data.") print(e) renpy.block_rollback() return None group = [] for i, x in enumerate(imported): if i == 0 and not x == self.name: # renpy.notify("Import failed: Wrong character.") return None for o in self.wardrobe_list: if x[0] == o.id: if not o.unlocked and not states.env.cheats: renpy.notify("Import failed: You don't own these items. Buy them first.") return None x[0] = o.clone() x[0].set_color(x[1]) group.append(x[0]) if group: # renpy.notify("Import successful!") outfit = DollOutfit(group, True) outfit.imported = path return outfit renpy.notify("Import failed: Unknown error.") return None def get_schedule(self): """Returns a list of outfits available for current time of day and weather conditions.""" schedule = [] for o in self.outfits: if o.unlocked and o.schedule["day" if states.env.daytime else "night"]: if states.env.weather == "overcast" and o.schedule["cloudy"]: schedule.append(o) elif states.env.weather in {"storm", "rain"} and o.schedule["rainy"]: schedule.append(o) elif states.env.weather in {"snow", "blizzard"} and o.schedule["snowy"]: schedule.append(o) elif states.env.weather in {"clear", "cloudy"} and not (o.schedule["cloudy"] or o.schedule["rainy"] or o.schedule["snowy"]): schedule.append(o) return schedule def equip_random_outfit(self): """Equips random outfit based on Outfits Schedule.""" schedule = self.get_schedule() if schedule: self.equip(renpy.random.choice(schedule)) def set_emote(self, emote): if isinstance(emote, str): path = posixpath.join(self.modpath, "characters", self.name, "poses", self.pose, "emote", emote) extensions = self.extensions for f in renpy.list_files(): fp, fn = os.path.split(f) fn, ext = os.path.splitext(fn) if not fp == path or not ext in extensions: continue self.emote = Image(f) break else: self.emote = emote self.is_stale() def clear_outfit_button_cache(self): if not settings.get("multithreading"): return DollThread.stop_all() for i in self.outfits: i._button.last_item = i._loading i.clear_button_cache()