# For the avoidance of doubt: you, the reader, are hereby authorized to use any # part of this patch in any way you choose, without limitation. # # This explicitly includes Time Wizard Studios, who may take any segments of # this patch, up to and including the whole, and integrate them into the base # game in any way they see fit, without credit or acknowledgement. # # (Mostly they probably shouldn't, because Patreon. But some of it isn't what # Patreon would care about.) # ============================================================================= # Changelog: # # v3.2: # - Fixed the accidental removal of everyone _but_ Jo's scenes from the # Gallery, introduced in 3.1. # # v3.1: # - Rewrote the larger part of the patch, based in part on a suggestion by # ZLZK (of F95). # # - Removed all use of the name 'Jo' (for now?), which could be confusing # if you started with the patch applied: # - Removed use of "Jo" by some characters which previous patch-versions # explicitly retained. # - Adjusted various fragments of text in the quest canonically titled # "Jo's Day", with added editorial meddling. # - Amended the scene-titles in the Gallery. # # - Converted the fix for the obscure `maxine_eggs` comment to use the # Ren'Py translation mechanism. Lightly tweaked the text for consistency. # # - Used the translation mechanism to alter a few other strings. # # v2: Fixed a bug which would crash the game whenever "???" spoke. # # v1: Original version. # ============================================================================= # Another Chance (henceforth usually "AC") v1.36 has some code that seems # to be part of a mechanism whereby the substituted name of a character can # be made different depending on who's talking about them. # # That would be particularly useful for us, if only it were functional. # Unfortunately, it's not: none of it's hooked up to anything, nor is there # anything shaped like its complement to hook it up to. It can't really be # made to work because Ren'Py 7.4.4 (which AC uses) falls _just_ short of # being able to hook into the necessary code. # # Fortunately, we can mostly work around that. Ren'Py doesn't quite provide # us with the data we need, but this _is_ Python, so we can monkey-patch in # what we want. # ============================================================================= init 1 python hide: # Virtually all uses of "Jo" or "[jo]" are in contexts associated with # either the main character or Flora. (They're not all speech; many, # perhaps most, are narration -- but fortunately, as in most VNs, the # narrator is the MC.) Thus, the following line alone would provide 99.9% # of the functionality of this patch. Character_jo.default_name = u"Mom" # As always, the remaining 0.1% requires 99.9% of the work. # # As of AC v1.36, there are only a handful of places in the entire game # where another character (or non-character UI text) names Jo directly. # # First, there are some UI-only hardcoded uses of "Jo". The hardcoded # scene-replay title text is perhaps easiest to fix: def fixup_replays(): import re rx = re.compile(r"^Jo's ") global replays_by_order replays_by_order = [ (a, rx.sub("Mom's ", title), c, d) for (a, title, c, d) in replays_by_order ] fixup_replays() # Also, there's one quest whose title contains it: Quest_jo_day.title = "Mom's Day" # ... along with some hint-adjacent flavor text in that quest; but that can # more easily be taken care of with the translation mechanism. (We do that # below.) # ========================================================================= # AC's BaseChar class sets an attribute `game._speaker` to the current # speaker of a Say command. We can't quite use that, for reasons that will # be explained along the way... but we can set up our own mechanism in # parallel instead. # Specifically, we store the current speaker in the Ren'Py-store variable # `_speaker_true`. (We could just use a variable in this hidden scope; but # this way we can easily get at the value from temporary diagnostic code # elsewhere, if needed.) def get_current_speaker(): # Note that, as is customary in Ren'Py, this may either be a character # object or a bare string. return getattr(renpy.store, "_speaker_true", None) # Set the current speaker in a particular scope, restoring it on exit. # # (This is part of the reason we can't use `_speaker`: it never gets unset. # If we tried to access it from within a Menu-statement, it would still # have its value from the last dialogue-utterance.) from contextlib import contextmanager @contextmanager def speaker_set_to(speaker): old_value = get_current_speaker() try: renpy.store._speaker_true = speaker yield None finally: renpy.store._speaker_true = old_value # Monkey-patching utility decorator. Replaces the specified method on a # class (or function on an object) with a new method provided. The old # method will be provided to the new method as a keyword argument. (In # theory we could override `super()`, but it's REALLY not worth the # trouble.) def replace_method(obj, method): def do_wrap(decorated_func): old_method = getattr(obj, method) def replacement_method(*args, **kwargs): return decorated_func(*args, _old_method=old_method, **kwargs) replacement_method.__name__ = old_method.__name__ setattr(obj, method, replacement_method) return None return do_wrap # Wrap BaseChar.__call__ so that the speaker is set... @replace_method(BaseChar, "__call__") def mark_speaker(self, *args, **kwargs): _old_method = kwargs.pop("_old_method") with speaker_set_to(self): return _old_method(self, *args, **kwargs) # ... and replace Character_jo.__str__ entirely, so that it checks the # current speaker to decide what to return. @replace_method(Character_jo, "__str__") def __str__(self, _old_method=None): speaker = get_current_speaker() # These characters (including None, which represents the MC when # speaking in menu text or narration) use the new default name, and # need no further processing. if speaker in {None, narrator, mc, flora}: return self.default_name # A previous version of this patch returned u"Jo" as some characters' # preferred form of reference: # # if speaker in {jo, mrsl, nurse, spinach, "???"}: # return u"Jo" # # Unfortunately, this doesn't quite work: if someone installs this # patch before they start, they'll be left wondering who the hell this # Jo person is when Mrs. L or the nurse eventually namedrop her. # Avoiding this would require explicitly injecting "Jo" somewhere where # the player will be guaranteed to see it, and I haven't (yet?) found a # non-awkward place to do that. # # At present, the other characters that refer to "[jo]" only do so when # addressing the protagonist, so the default below still works. # Note that this will fail if "[jo]" ever heads a sentence. No attempt # is made to detect that: it must be handled on a case-by-case basis. # (TWS could handle it by using "[jo!c]" instead; but there's only one # reason to do that, and they probably shouldn't.) return u'your mom' # You'd think that would cover it. Unfortunately, that covers too much. # # It may seem intuitive that a Say-statement's evaluation would happen as # part of ordinary screen layout. This is the opposite of the truth: screen # layout is performed ** as part of Say-statement evaluation **. # # If `str(jo)` is evaluated during screen layout, its current value will # depend on which character, if any, happens to be speaking at the time. If # the MC is speaking with another character while the result of that # evaluation is on-screen, it'll flip back and forth between "Mom" and # "your mom" as the characters alternate utterances. # # This is not hypothetical. In v1.36, it happens in the "phone_app_call" # screen when calling Jo. (Coincidentally, this screen is also the other # reason we can't use `_speaker`: it's it's accessed during screen layout # to position the MC's dialogue, while we need something that's `None` in # that same phase!) # # There isn't any scope that we can reasonably intercept in which # dialogue-text-substitution happens but screen-evaluation doesn't: the # former happens in `ADVCharacter.__call__`, which also invokes the latter. # However, there _is_ an interceptable scope in which screen-evaluation # happens, but text-substitution does not. # # So we just intercept that too. That's probably as sharp as we can get. @replace_method(ADVCharacter, "do_display") def mark_speaker(self, *args, **kwargs): _old_method = kwargs.pop("_old_method") with speaker_set_to(None): return _old_method(self, *args, **kwargs) # That's not great, because it involves monkey-patching the framework # instead of just the game. It should still work up to Ren'Py 8.1.3 (and # it's not likely AC will ever update its Ren'Py version anyway), but it # still leaves a foul taste in my mouth. # # If we knew for sure that "phone_app_call" was the only place where that # could happen, we could instead just monkey-patch that one screen... def fixup_phone(): __phonescreen = renpy.display.screen.get_screen_variant("phone_app_call",None) __phonescreen.function.children[1].children[0].positional[0] = \ u'contact.default_name' # fixup_phone() # ...but this is fragile in the face of minor stylistic changes to the # screen(s) in question, won't cover any uses in new screens while the game # is in development, and _still_ involves mucking around in Ren'Py # internals. # With the generic part out of the way, we turn to local and specific # adjustments. The Ren'Py translation mechanism turns out to be perfect for # this, so we define a fake language we're translating into. define config.language = "ipatch" # However: beyond this point necessarily lie artistic interpretations and, # therefore, decisions. Abandon all pretense to objectivity, ye who enter here. translate ipatch quest_flora_bonsai_bed_16ea41b6: # flora bed flirty "Being your roommate has taught me everything I need to know about purifying unholy taint." flora bed flirty "Being your sister has taught me everything I need to know about purifying unholy taint." translate ipatch quest_flora_squid_trail_flora_e6f80e4d: # mc "Maybe it slipped into the principal's office?" mc "Maybe it slipped into [jo]'s office?" translate ipatch quest_jo_day_amends_2bdfe697: # mc "Because, let's be honest, you're the mother of every student in Newfall High." mc "Well, give or take half a year. But better late than never!" "Later than you know, [jo]. Later than you know." # This could be more specific; it's been left vague for now to avoid the # possibility of contradictions with new material. translate ipatch quest_jo_day_picnic_3e4a8f94: # "She kept trying to make out with me and called me by her husband's name..." "She kept trying to make out with me and calling me the wrong name..." translate ipatch strings: # Guides and flavor text for the quest "jo_day". old "Today is the big day. Jo's day!" new "Today is the big day. Mom's day!" old "Tomorrow is the big day. Jo's day!" new "Tomorrow is the big day. Mom's day!" old "Jo's day or my day? Our day!" new "Mom's day or my day? Our day!" # This bit is incredibly niche; it's intended to fix a very minor bug # introduced into the `quest_maxine_eggs_bathroom_encounter` conversation... # and also to clean up the associated phrasing a little bit, because several of # the relevant characters probably don't have a locker. translate ipatch quest_maxine_eggs_bathroom_encounter_fa04a789: # lindsey eyeroll "Nope. Have you checked [not_best_girl]'s locker?" $ not_best_girl = max([jo, flora, mrsl, isabelle, kate, jacklyn, nurse, maxine], key=lambda c: c.love) if not_best_girl is jo: $ _fa04a789_storage = "office" elif not_best_girl is nurse: $ _fa04a789_storage = "office" $ not_best_girl = "the nurse" elif not_best_girl is mrsl: $ _fa04a789_storage = "desk" else: $ _fa04a789_storage = "locker" lindsey eyeroll "Nope. Have you checked [not_best_girl]'s [_fa04a789_storage]?"