diff --git a/etc/Configrc_dev.prc b/etc/Configrc_dev.prc new file mode 100644 index 0000000..b9b11fd --- /dev/null +++ b/etc/Configrc_dev.prc @@ -0,0 +1,46 @@ +# Local dev overrides (safe to edit/delete). + +# Enable shard flow breadcrumbs + hang watchdog stack dumps. +shard-debug 1 + +# Make local testing easier (Astron dev login is keyed off username in astron/databases/accounts.json). +required-login auto + +# Launcher/UI revamp +want-modern-launcher-ui 1 +# Automatically start Astron/UberDOG/AI on client launch (Windows dev flow). +auto-start-local-servers 1 +# If 1, server scripts open visible consoles (useful for debugging). +local-servers-show-consoles 0 +# If 1, pipe server stdout/stderr into the client console. +local-servers-forward-logs 1 +# Even if MD is already running, spawn UberDOG/AI so their logs can be forwarded. +local-servers-always-spawn-python 1 +# If login succeeds then drops (10053) or loops error 100, CA may be up while UberDOG died — after killing +# orphaned ppython UD/AI, uncomment ONE of: +# local-servers-force-python-spawn 1 +# local-servers-skip-python-if-ca-open 0 + +# Pick-a-Toon: render UI over a TTC backdrop when available. +want-pick-a-toon-ttc-backdrop 1 +# Optional: enable OutdoorLighting on the TTC backdrop (can be expensive / risky). +pick-a-toon-ttc-backdrop-want-lighting 0 + +# If something still insists on a playToken path, provide one. +fake-playtoken dev + +# Extra visibility. +default-directnotify-level info +notify-level-OTPClientRepository info + +# Off during normal play: when enabled, every C++ event updates a Python dict (expensive +# during loader/interest bursts and makes hitches worse). +eventmanager-debug-flood #f +# Cheap flood diagnosis: for small persistent floods (processed ~10-100), +# use stride=1 so the "top names" list actually populates. +eventmanager-flood-approx-stride 1 +# Never drain the queue on flood: clearing it drops async/interest completion events +# and the client fails to finish shard entry or hood load (silent exit / hang). +eventmanager-drain-on-flood #f +# See Configrc.prc: huge values = one doEvents() blocks for seconds; 50k/frame is a balance. +eventmanager-max-events-per-frame 50000 \ No newline at end of file diff --git a/otp/otpbase/OTPBase.py b/otp/otpbase/OTPBase.py index 2c61ef1..99d3856 100644 --- a/otp/otpbase/OTPBase.py +++ b/otp/otpbase/OTPBase.py @@ -1,5 +1,6 @@ from direct.showbase.ShowBase import ShowBase from panda3d.core import Camera, TPLow, VBase4, ColorWriteAttrib, Filename, getModelPath, NodePath, ConfigVariableBool, ConfigVariableDouble +from direct.task.TaskManagerGlobal import taskMgr from . import OTPRender import time import math @@ -40,8 +41,62 @@ class OTPBase(ShowBase): else: base.cam.node().setCameraMask(OTPRender.MainCameraBitmask | OTPRender.EnviroCameraBitmask) taskMgr.setupTaskChain('net') + # Some render-to-texture / post-process paths can leave the main window's + # DisplayRegions cropped or pixel-zoomed. Nudge viewports back to full + # window shortly after startup (and on demand via repairMainViewports()). + try: + self._viewportRepairFrames = 0 + taskMgr.doMethodLater(0.0, self._repairMainViewportsTask, 'otpRepairMainViewports', extraArgs=[], appendTask=True) + except Exception: + pass + try: + # Hard fallback for stubborn driver/runtime viewport corruption: + # keep forcing full-window DisplayRegions every frame. + if ConfigVariableBool('force-full-window-display-region', True).value: + taskMgr.remove('otpForceFullWindowDisplayRegions') + taskMgr.add(self._forceFullWindowDisplayRegionsTask, 'otpForceFullWindowDisplayRegions', sort=10000) + except Exception: + pass return + def repairMainViewports(self) -> None: + """Force all main-window DisplayRegions to cover the full framebuffer. + + This fixes the common symptom where the scene/GUI only occupies the + bottom-left quadrant after a graphics pipeline hiccup. + """ + try: + win = base.win + if not win: + return + try: + win.setPixelZoom(1) + except Exception: + pass + n = win.getNumDisplayRegions() + for i in range(n): + try: + dr = win.getDisplayRegion(i) + if not dr: + continue + dr.setDimensions(0.0, 1.0, 0.0, 1.0) + if dr.supportsPixelZoom(): + dr.setPixelZoom(1) + except Exception: + pass + except Exception: + pass + + def _repairMainViewportsTask(self, task): + self.repairMainViewports() + self._viewportRepairFrames += 1 + # Run for a handful of frames to survive delayed window reconfiguration. + return task.again if self._viewportRepairFrames < 8 else task.done + + def _forceFullWindowDisplayRegionsTask(self, task): + self.repairMainViewports() + return task.cont + def setTaskChainNetThreaded(self): if base.config.GetBool('want-threaded-network', 0): taskMgr.setupTaskChain('net', numThreads=1, frameBudget=0.001, threadPriority=TPLow) @@ -135,7 +190,9 @@ class OTPBase(ShowBase): self.pixelZoomCamHistory = 2.0 self.pixelZoomCamMovedList = [] self.pixelZoomStarted = None - flag = self.config.GetBool('enable-pixel-zoom', True) + # Pixel zoom shrinks the render into a corner at higher zoom factors. + # Default it off for desktop builds unless explicitly enabled. + flag = self.config.GetBool('enable-pixel-zoom', False) self.enablePixelZoom(flag) return diff --git a/toontown/hood/QuietZoneState.py b/toontown/hood/QuietZoneState.py index a7011c0..3bc1825 100644 --- a/toontown/hood/QuietZoneState.py +++ b/toontown/hood/QuietZoneState.py @@ -83,7 +83,19 @@ class QuietZoneState(StateData.StateData): return def _start(self, requestStatus): - base.transitions.fadeScreen(0.3) + # Allow callers (eg. Pick-a-Toon TTC preload) to suppress the quiet-zone + # fade while still performing interest setup/network handoff. + if requestStatus.get('noFade'): + base.transitions.noTransitions() + else: + base.transitions.fadeScreen(0.3) + try: + if ConfigVariableBool('want-async-zone-prefetch', True).value: + ld = getattr(base, 'loader', None) + if ld and hasattr(ld, 'schedulePrefetchForQuietZone'): + ld.schedulePrefetchForQuietZone(requestStatus) + except Exception: + pass self.fsm.request('waitForQuietZoneResponse') def getRequestStatus(self): @@ -310,10 +322,18 @@ class QuietZoneState(StateData.StateData): if __astron__: def getStreetViszones(self, zoneId): - visZones = [ZoneUtil.getBranchZone(zoneId)] - # Assuming that the DNA have been loaded by bulk load before this (see Street.py). - loader = base.cr.playGame.hood.loader - visZones += [loader.node2zone[x] for x in loader.nodeDict[zoneId]] + # When enabled, request interest in the entire street at once (all visgroups). + if ConfigVariableBool('street-load-whole', 1).value: + loader = base.cr.playGame.hood.loader + visZones = set(loader.zoneDict.keys()) + visZones.add(ZoneUtil.getBranchZone(zoneId)) + visZones.add(zoneId) + visZones = sorted(visZones) + else: + visZones = [ZoneUtil.getBranchZone(zoneId)] + # Assuming that the DNA have been loaded by bulk load before this (see Street.py). + loader = base.cr.playGame.hood.loader + visZones += [loader.node2zone[x] for x in loader.nodeDict[zoneId]] self.notify.debug(f'getStreetViszones(zoneId={zoneId}): returning visZones: {visZones}') return visZones @@ -358,6 +378,11 @@ class QuietZoneState(StateData.StateData): def enterWaitForSetZoneComplete(self): # self.notify.debug('enterWaitForSetZoneComplete(requestStatus=' + str(self._requestStatus) + ')') if not self.Disable: + if ConfigVariableBool('shard-debug', 0).value: + try: + self.notify.info(f"[ShardDbg] QuietZoneState.enterWaitForSetZoneComplete setZoneDoneEvent={base.cr.getLastSetZoneDoneEvent()!r}") + except Exception: + pass base.cr.handlerArgs = self._requestStatus if base.slowQuietZone: @@ -405,12 +430,51 @@ class QuietZoneState(StateData.StateData): base.cr.handlerArgs = self._requestStatus self._onShardEvent = localAvatar.getArrivedOnDistrictEvent() self.waitForDatabase('WaitForLocalAvatarOnShard') + if ConfigVariableBool('shard-debug', 0).value: + try: + self.notify.info(f"[ShardDbg] QuietZoneState.enterWaitForLocalAvatarOnShard onShardEvent={self._onShardEvent!r} shard={getattr(localAvatar, 'defaultShard', None)!r} zone={getattr(localAvatar, 'zoneId', None)!r}") + except Exception: + pass + # If we wedge hard, try to force a Python stack dump later. + try: + import faulthandler + faulthandler.dump_traceback_later(ConfigVariableDouble('localav-onshard-timeout', 20.0).value + 5.0, repeat=False) + except Exception: + pass + # Safety net: if the local avatar never arrives on the district, + # the client appears to hard-freeze on a loading screen forever. + if ConfigVariableBool('shard-debug', 0).value: + timeout = ConfigVariableDouble('localav-onshard-timeout', 20.0).value + taskMgr.remove('localAvOnShardTimeout') + taskMgr.doMethodLater(timeout, self._localAvOnShardTimeout, 'localAvOnShardTimeout') if localAvatar.isGeneratedOnDistrict(localAvatar.defaultShard): self._announceDone() else: self.acceptOnce(self._onShardEvent, self._announceDone) + def _localAvOnShardTimeout(self, task): + try: + self.notify.warning('[ShardDbg] timed out waiting for localAvatar arrived-on-district; returning to noConnection') + except Exception: + pass + try: + self.ignore(self._onShardEvent) + except Exception: + pass + try: + base.cr.loginFSM.request('noConnection') + except Exception: + try: + base.userExit() + except Exception: + pass + return Task.done + def _announceDone(self): + try: + taskMgr.remove('localAvOnShardTimeout') + except Exception: + pass base.localAvatar.startChat() if base.endlessQuietZone: self._dequeue() diff --git a/toontown/hood/SkyUtil.py b/toontown/hood/SkyUtil.py index bb17088..5b22d35 100644 --- a/toontown/hood/SkyUtil.py +++ b/toontown/hood/SkyUtil.py @@ -4,6 +4,7 @@ from direct.task.Task import Task from direct.directnotify import DirectNotifyGlobal notify = DirectNotifyGlobal.directNotify.newCategory('SkyUtil') + def cloudSkyTrack(task): task.h += globalClock.getDt() * 0.25 if task.cloud1.isEmpty() or task.cloud2.isEmpty(): @@ -14,22 +15,76 @@ def cloudSkyTrack(task): return Task.cont -def startCloudSky(hood, parent = camera, effects = CompassEffect.PRot | CompassEffect.PZ): - hood.sky.reparentTo(parent) - hood.sky.setDepthTest(0) - hood.sky.setDepthWrite(0) - hood.sky.setBin('background', 100) - hood.sky.find('**/Sky').reparentTo(hood.sky, -1) - hood.sky.reparentTo(parent) - hood.sky.setZ(0.0) - hood.sky.setHpr(0.0, 0.0, 0.0) - ce = CompassEffect.make(NodePath(), effects) - hood.sky.node().setEffect(ce) +def _wantProceduralSky() -> bool: + """Returns True if the player has enabled the procedural sky system.""" + try: + from toontown.hood import OutdoorLighting as osl + if getattr(osl, '_OUTDOOR_SHADER_BISECT_LEVEL', 0) < 1: + return False + except Exception: + pass + try: + from toontown.toonbase.ToonBaseGlobal import base + val = base.settings.getSetting('want-procedural-sky', True) + if val is not None: + return bool(val) + except Exception: + pass + try: + from panda3d.core import ConfigVariableBool + return ConfigVariableBool('want-procedural-sky', True).value + except Exception: + return True + + +def startCloudSky(hood, parent=camera, + effects=CompassEffect.PRot | CompassEffect.PZ): + """Set up the sky for a hood that has rotating clouds. + + When 'want-procedural-sky' is enabled (default), OutdoorLighting will hide + the legacy model sky once the procedural GLSL dome is successfully attached. + We keep the legacy sky node intact so it can act as a fallback if shaders + are unavailable. + """ + # Important: OutdoorLighting owns ProceduralSky attach/hide/show behavior. + # Do NOT delete/replace hood.sky here; if the shader fails to load, we need + # the legacy model sky to remain available as a fallback. + + # ── Legacy model sky path (ProceduralSky disabled or shaders unavailable) ── + try: + hood.sky.reparentTo(parent) + hood.sky.setDepthTest(0) + hood.sky.setDepthWrite(0) + hood.sky.setBin('background', 100) + except Exception: + pass + try: + hood.sky.find('**/Sky').reparentTo(hood.sky, -1) + except Exception: + pass + try: + hood.sky.reparentTo(parent) + hood.sky.setZ(0.0) + hood.sky.setHpr(0.0, 0.0, 0.0) + ce = CompassEffect.make(NodePath(), effects) + hood.sky.node().setEffect(ce) + except Exception: + pass + + # If ProceduralSky is enabled, OutdoorLighting will typically hide the legacy + # sky; skip the legacy rotating-cloud task to avoid wasted work. + if _wantProceduralSky(): + return + + # Start the rotating-cloud animation task (legacy only). skyTrackTask = Task(hood.skyTrack) skyTrackTask.h = 0 - skyTrackTask.cloud1 = hood.sky.find('**/cloud1') - skyTrackTask.cloud2 = hood.sky.find('**/cloud2') - if not skyTrackTask.cloud1.isEmpty() and not skyTrackTask.cloud2.isEmpty(): - taskMgr.add(skyTrackTask, 'skyTrack') - else: - notify.warning("Couln't find clouds!") + try: + skyTrackTask.cloud1 = hood.sky.find('**/cloud1') + skyTrackTask.cloud2 = hood.sky.find('**/cloud2') + if not skyTrackTask.cloud1.isEmpty() and not skyTrackTask.cloud2.isEmpty(): + taskMgr.add(skyTrackTask, 'skyTrack') + else: + notify.warning("Couldn't find clouds!") + except Exception: + notify.warning("Couldn't find clouds!") diff --git a/toontown/hood/ZoneUtil.py b/toontown/hood/ZoneUtil.py index de22d9b..033cf1e 100644 --- a/toontown/hood/ZoneUtil.py +++ b/toontown/hood/ZoneUtil.py @@ -198,12 +198,9 @@ def isInterior(zoneId): def overrideOn(branch, exteriorList, interiorList): - global tutorialDict - if tutorialDict: - zoneUtilNotify.warning('setTutorialDict: tutorialDict is already set!') - tutorialDict = {'branch': branch, - 'exteriors': exteriorList, - 'interiors': interiorList} + # Tutorial zone overrides create hard locks by rewriting hood/zone routing. + # This project disables tutorial zone locking entirely. + return def overrideOff(): diff --git a/toontown/toonbase/ZonePrefetchCatalog.py b/toontown/toonbase/ZonePrefetchCatalog.py new file mode 100644 index 0000000..b1057e4 --- /dev/null +++ b/toontown/toonbase/ZonePrefetchCatalog.py @@ -0,0 +1,51 @@ +""" +Curated model paths for background async prefetch during quiet-zone transitions. + +Paths are best-effort; the loader uses okMissing so missing assets are skipped. +Profile real zone loads and append heavy models that still hit the disk here. +""" + +from toontown.hood import ZoneUtil +from toontown.toonbase import ToontownGlobals as TG + + +def _uniq(seq): + out = [] + seen = set() + for p in seq: + if not p or p in seen: + continue + seen.add(p) + out.append(p) + return out + + +def paths_for_quiet_zone(request_status): + if not request_status: + return [] + loader = request_status.get('loader') + hood_id = int(request_status.get('hoodId', 0) or 0) + try: + cz = ZoneUtil.getCanonicalZoneId(hood_id) + except Exception: + cz = hood_id + + core = [ + 'phase_3.5/models/gui/inventory_gui', + 'phase_3/models/gui/dialog_box_gui', + 'phase_3/models/props/drop_shadow', + 'phase_3/models/gui/chat_button_gui', + ] + + extra = [] + if loader == 'safeZoneLoader': + if cz == TG.ToontownCentral: + extra.append('phase_4/models/modules/trolley_station_TT') + elif cz == TG.GoofySpeedway: + extra.append('phase_6/models/golf/golf_hub2') + elif cz == TG.OutdoorZone: + extra.append('phase_6/models/golf/golf_geyser_model') + elif loader == 'townLoader': + extra.append('phase_4/models/modules/trolley_station_TT') + + return _uniq(core + extra)