From e681053ea2ee40ad0db8c1a76e1b72843d4c5061 Mon Sep 17 00:00:00 2001 From: Toontown Super <108632596+ToontownSuperForeverMVP@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:08:36 -0400 Subject: [PATCH] gui: add modern loading screen and asset preview overlay --- toontown/distributed/PlayGame.py | 50 ++- toontown/toonbase/ToontownLoader.py | 128 +++++- toontown/toontowngui/LoadingAssetPreview.py | 223 ++++++++++ toontown/toontowngui/ModernLoadingScreen.py | 414 ++++++++++++++++++ toontown/toontowngui/ToontownLoadingScreen.py | 68 ++- 5 files changed, 863 insertions(+), 20 deletions(-) create mode 100644 toontown/toontowngui/LoadingAssetPreview.py create mode 100644 toontown/toontowngui/ModernLoadingScreen.py diff --git a/toontown/distributed/PlayGame.py b/toontown/distributed/PlayGame.py index 470b62b..31ed8a3 100644 --- a/toontown/distributed/PlayGame.py +++ b/toontown/distributed/PlayGame.py @@ -105,6 +105,7 @@ class PlayGame(StateData.StateData): self.hood = None self.quietZoneDoneEvent = uniqueName('quietZoneDone') self.quietZoneStateData = None + self._reusePickAToonTtcHood = False return def enter(self, hoodId, zoneId, avId): @@ -120,13 +121,26 @@ class PlayGame(StateData.StateData): else: loaderName = ZoneUtil.getLoaderName(zoneId) whereName = ZoneUtil.getToonWhereName(zoneId) - self.fsm.request('quietZone', [{'loader': loaderName, + requestStatus = {'loader': loaderName, 'where': whereName, 'how': 'teleportIn', 'hoodId': hoodId, 'zoneId': zoneId, 'shardId': None, - 'avId': avId}]) + 'avId': avId} + + # Revamp: when coming from Pick-a-Toon, Toontown Central may already be + # loaded and visible behind the GUI. We still must go through quietZone + # (FSM/network interest setup), but we can suppress the fade and reuse + # the already-loaded TTC hood in handleWaitForSetZoneResponse. + try: + preHood = getattr(base.cr, '_pickAToonTTCBackdrop', None) + if preHood and ZoneUtil.getCanonicalZoneId(hoodId) == ToontownGlobals.ToontownCentral and requestStatus['loader'] == 'safeZoneLoader': + requestStatus['noFade'] = True + except Exception: + pass + + self.fsm.request('quietZone', [requestStatus]) return def exit(self): @@ -244,6 +258,34 @@ class PlayGame(StateData.StateData): loaderName = requestStatus['loader'] avId = requestStatus.get('avId', -1) ownerId = requestStatus.get('ownerId', avId) + + # Revamp: if we already preloaded Toontown Central during Pick-a-Toon, + # reuse it only when we're actually entering TTC playground. Otherwise + # drop the preload first — otherwise the backdrop and the real hood + # both load (assertion failures). + try: + preHood = getattr(base.cr, '_pickAToonTTCBackdrop', None) + reusePickAToonTTC = ( + preHood + and canonicalHoodId == ToontownGlobals.ToontownCentral + and loaderName == 'safeZoneLoader' + ) + if preHood and not reusePickAToonTTC: + base.cr.cleanupPickAToonTTCBackdrop() + elif reusePickAToonTTC: + self._reusePickAToonTtcHood = True + self.hood = preHood + base.cr._pickAToonTTCBackdrop = None + base.cr._pickAToonTTCBackdropRequestStatus = None + try: + if hasattr(self.hood, 'loader') and hasattr(self.hood.loader, 'geom'): + self.hood.loader.geom.reparentTo(render) + except Exception: + pass + return + except Exception: + pass + if base.config.GetBool('want-qa-regression', 0): self.notify.info('QA-REGRESSION: NEIGHBORHOODS: Visit %s' % hoodName) count = ToontownGlobals.hoodCountMap[canonicalHoodId] @@ -300,7 +342,9 @@ class PlayGame(StateData.StateData): self.quietZoneStateData.exit() self.quietZoneStateData.unload() self.quietZoneStateData = None - loader.endBulkLoad('hood') + if not getattr(self, '_reusePickAToonTtcHood', False): + loader.endBulkLoad('hood') + self._reusePickAToonTtcHood = False else: self.handleLeftQuietZone() return diff --git a/toontown/toonbase/ToontownLoader.py b/toontown/toonbase/ToontownLoader.py index a23a0c8..ecf491b 100644 --- a/toontown/toonbase/ToontownLoader.py +++ b/toontown/toonbase/ToontownLoader.py @@ -3,6 +3,7 @@ from panda3d.toontown import * from direct.directnotify.DirectNotifyGlobal import * from direct.showbase import Loader from toontown.toontowngui import ToontownLoadingScreen +import time class ToontownLoader(Loader.Loader): TickPeriod = 0.01 @@ -14,9 +15,74 @@ class ToontownLoader(Loader.Loader): self.loadingScreen = ToontownLoadingScreen.ToontownLoadingScreen() self._tickCounter = 0 self._tickSkip = 10 + self._prefetch_handles = [] return + def cancelPrefetchRequests(self): + for h in list(self._prefetch_handles): + try: + if hasattr(h, 'cancel') and not h.cancelled() and not h.done(): + h.cancel() + except Exception: + pass + self._prefetch_handles = [] + + def schedulePrefetchForQuietZone(self, request_status): + if not ConfigVariableBool('want-async-zone-prefetch', True).value: + return + self.cancelPrefetchRequests() + try: + from toontown.toonbase import ZonePrefetchCatalog + + paths = ZonePrefetchCatalog.paths_for_quiet_zone(request_status) + except Exception: + paths = [] + for i, path in enumerate(paths): + self._startPrefetchModel(path, priority=max(1, 80 - i)) + + def _startPrefetchModel(self, path, priority=10): + holder = {'cb': None} + + def _done(model): + cb = holder['cb'] + try: + if cb is not None and cb in self._prefetch_handles: + self._prefetch_handles.remove(cb) + except Exception: + pass + try: + if model is not None and not model.isEmpty(): + ModelPool.addModel(path, model.node()) + except Exception: + pass + + try: + cb = Loader.Loader.loadModel( + self, + path, + callback=_done, + blocking=False, + okMissing=True, + priority=priority, + ) + holder['cb'] = cb + if cb is not None and hasattr(cb, 'requests'): + self._prefetch_handles.append(cb) + except Exception: + pass + + + def _notifyModernAsset(self, path, node=None, texture=None, kind='model'): + try: + ml = getattr(self.base, 'modernLoading', None) + if ml is None or not ml.enabled(): + return + ml.on_asset_loaded(path=path, model=node, texture=texture, kind=kind) + except Exception: + pass + def destroy(self): + self.cancelPrefetchRequests() self.loadingScreen.destroy() del self.loadingScreen Loader.Loader.destroy(self) @@ -28,9 +94,26 @@ class ToontownLoader(Loader.Loader): Loader.Loader.notify.warning("Tried to start a block ('%s'), but am already in a block ('%s')" % (name, self.blockName)) return None self.inBulkBlock = 1 + self._bulkPrevTickSkip = self._tickSkip + self._tickSkip = 1 self._lastTickT = globalClock.getRealTime() self.blockName = name - self.loadingScreen.begin(range, label, gui, tipCategory) + try: + ml = getattr(self.base, 'modernLoading', None) + if ml and ml.enabled(): + ml.enter_bulk_load(name, label, range) + if ConfigVariableBool('minimal-legacy-loading-with-modern', True).value: + gui = 0 + except Exception: + pass + minimal = False + try: + ml = getattr(self.base, 'modernLoading', None) + if ml and ml.enabled() and ConfigVariableBool('minimal-legacy-loading-with-modern', True).value: + minimal = True + except Exception: + pass + self.loadingScreen.begin(range, label, gui, tipCategory, minimal=minimal) return None def endBulkLoad(self, name): @@ -41,19 +124,33 @@ class ToontownLoader(Loader.Loader): Loader.Loader.notify.warning("Tried to end a block ('%s'), other then the current one ('%s')" % (name, self.blockName)) return None self.inBulkBlock = None + self._tickSkip = self._bulkPrevTickSkip expectedCount, loadedCount = self.loadingScreen.end() now = globalClock.getRealTime() Loader.Loader.notify.info("At end of block '%s', expected %s, loaded %s, duration=%s" % (self.blockName, expectedCount, loadedCount, now - self._loadStartT)) + try: + ml = getattr(self.base, 'modernLoading', None) + if ml and ml.enabled(): + ml.leave_bulk_load() + except Exception: + pass return def abortBulkLoad(self): if self.inBulkBlock: Loader.Loader.notify.info("Aborting block ('%s')" % self.blockName) self.inBulkBlock = None + self._tickSkip = self._bulkPrevTickSkip self.loadingScreen.abort() + try: + ml = getattr(self.base, 'modernLoading', None) + if ml and ml.enabled(): + ml.leave_bulk_load() + except Exception: + pass return def tick(self): @@ -66,18 +163,42 @@ class ToontownLoader(Loader.Loader): self._lastTickT += self.TickPeriod self.loadingScreen.tick() try: - base.cr.considerHeartbeat() + ml = getattr(self.base, 'modernLoading', None) + if ml and ml.enabled(): + if not ConfigVariableBool('want-zero-load-ui', False).value: + ml.set_progress(self.loadingScreen.get_progress_fraction() * 100.0) + except Exception: + pass + try: + if getattr(self.base, 'cr', None): + self.base.cr.considerHeartbeat() except: pass + # Critical: bulk loads can otherwise run as one giant "frame" + # (unplayable hitch). Yield so the OS and render thread can + # breathe, spreading the work across multiple frames. + try: + time.sleep(0) + except Exception: + pass def loadModel(self, *args, **kw): ret = Loader.Loader.loadModel(self, *args, **kw) + if kw.get('callback') is not None: + return ret self.tick() + path = str(args[0]) if args else None + if ret is not None and not ret.isEmpty(): + self._notifyModernAsset(path, node=ret) + else: + self._notifyModernAsset(path, node=None) return ret def loadFont(self, *args, **kw): ret = Loader.Loader.loadFont(self, *args, **kw) self.tick() + path = str(args[0]) if args else None + self._notifyModernAsset(path, node=None, kind='font') return ret def loadTexture(self, texturePath, alphaPath = None, okMissing = False): @@ -85,16 +206,19 @@ class ToontownLoader(Loader.Loader): self.tick() if alphaPath: self.tick() + self._notifyModernAsset(str(texturePath), texture=ret) return ret def loadSfx(self, soundPath): ret = Loader.Loader.loadSfx(self, soundPath) self.tick() + self._notifyModernAsset(str(soundPath), kind='audio') return ret def loadMusic(self, soundPath): ret = Loader.Loader.loadMusic(self, soundPath) self.tick() + self._notifyModernAsset(str(soundPath), kind='audio') return ret def loadDNAFileAI(self, dnaStore, dnaFile): diff --git a/toontown/toontowngui/LoadingAssetPreview.py b/toontown/toontowngui/LoadingAssetPreview.py new file mode 100644 index 0000000..aa899fe --- /dev/null +++ b/toontown/toontowngui/LoadingAssetPreview.py @@ -0,0 +1,223 @@ +""" +Live 3D / texture preview for the unified loading UI. + +Uses an offscreen buffer with Camera.setScene() so preview geometry never mixes +with the main world render. +""" + +import os +from typing import Optional + +from direct.directnotify import DirectNotifyGlobal +from direct.showbase.ShowBaseGlobal import base +from direct.task import Task +from panda3d.core import ( + AmbientLight, + CardMaker, + DirectionalLight, + NodePath, + PerspectiveLens, + Texture, + TransparencyAttrib, + VBase4, +) + +notify = DirectNotifyGlobal.directNotify.newCategory('LoadingAssetPreview') + +_PREVIEW_CLEAR = VBase4(0.08, 0.12, 0.19, 1.0) + + +class LoadingAssetPreview: + def __init__(self): + self._buffer = None + self._cam_np = None + self._world = NodePath('loadingAssetPreviewWorld') + self._holder = self._world.attachNewNode('assetHolder') + self._card = None + self._border = None + self._spin_task = None + self._last_note_t = 0.0 + self._min_interval = 0.06 + + def attach_card(self, parent: NodePath, pos, frame, sort: int = 100002): + """ + parent: aspect2d parent + pos: (x, y, z) in aspect2d space + frame: (left, right, bottom, top) CardMaker frame (relative scale) + """ + self.detach_card() + if not base.win: + return + try: + w, h = 512, 288 + self._buffer = base.win.makeTextureBuffer('loadingAssetPreview', w, h, to_ram=False, fbp=False) + self._buffer.setClearColor(True) + self._buffer.setClearColor(_PREVIEW_CLEAR) + + amb = AmbientLight('lap_amb') + amb.setColor(VBase4(0.5, 0.5, 0.58, 1)) + anp = self._world.attachNewNode(amb) + self._world.setLight(anp) + sun = DirectionalLight('lap_sun') + sun.setColor(VBase4(0.92, 0.9, 0.82, 1)) + snp = self._world.attachNewNode(sun) + snp.setHpr(-68, -32, 0) + self._world.setLight(snp) + + lens = PerspectiveLens() + lens.setFov(40) + lens.setNearFar(0.08, 500.0) + self._cam_np = base.makeCamera( + self._buffer, + scene=self._world, + lens=lens, + aspectRatio=float(w) / float(h), + clearColor=_PREVIEW_CLEAR, + sort=-60, + ) + self._cam_np.setPos(0, -16, 6.5) + self._cam_np.lookAt(0, 0, 3.0) + + tex = self._buffer.getTexture() + bl, br, bb, bt = frame + bwide = 0.02 + bcm = CardMaker('lapBorder') + bcm.setFrame(bl - bwide, br + bwide, bb - bwide, bt + bwide) + self._border = parent.attachNewNode(bcm.generate()) + self._border.setColor(0.12, 0.65, 0.95, 0.55) + self._border.setTransparency(TransparencyAttrib.M_alpha) + self._border.setBin('fixed', sort - 1) + self._border.setPos(*pos) + + cm = CardMaker('lapCard') + cm.setFrame(bl, br, bb, bt) + self._card = parent.attachNewNode(cm.generate()) + self._card.setTexture(tex) + self._card.setTransparency(TransparencyAttrib.M_alpha) + self._card.setBin('fixed', sort) + self._card.setPos(*pos) + except Exception as e: + notify.warning('loading asset preview unavailable: %s' % e) + self.detach_card() + + def detach_card(self): + self._stop_spin() + if self._card: + try: + self._card.removeNode() + except Exception: + pass + self._card = None + if self._border: + try: + self._border.removeNode() + except Exception: + pass + self._border = None + self._teardown_buffer() + self._clear_holder() + + def _teardown_buffer(self): + if self._buffer: + try: + base.graphicsEngine.removeWindow(self._buffer) + except Exception: + pass + self._buffer = None + self._cam_np = None + + def _clear_holder(self): + self._stop_spin() + try: + for c in self._holder.getChildren(): + c.removeNode() + except Exception: + pass + + def _stop_spin(self): + if self._spin_task: + try: + base.taskMgr.remove(self._spin_task) + except Exception: + pass + self._spin_task = None + + def _spin(self, task): + try: + self._holder.setH(self._holder.getH() + 0.9) + except Exception: + pass + return Task.cont + + def note_asset( + self, + path: Optional[str], + model: Optional[NodePath] = None, + texture: Optional[Texture] = None, + force: bool = False, + ): + if not self._buffer or not self._card: + return + now = base.globalClock.getRealTime() + if not force and (now - self._last_note_t) < self._min_interval: + return + self._last_note_t = now + + self._clear_holder() + self._stop_spin() + + if model is not None and not model.isEmpty(): + try: + inst = model.copyTo(self._holder) + self._frame_node(inst) + self._holder.setH(15) + self._spin_task = base.taskMgr.add(self._spin, 'loadingAssetSpin', priority=50) + except Exception as e: + notify.debug('model preview failed: %s' % e) + return + + if texture is not None: + try: + cm = CardMaker('texFlat') + cm.setFrame(-6, 6, -6, 6) + flat = self._holder.attachNewNode(cm.generate()) + flat.setTexture(texture) + flat.setTransparency(TransparencyAttrib.M_alpha) + flat.lookAt(0, -1, 0) + except Exception as e: + notify.debug('texture preview failed: %s' % e) + + def _frame_node(self, np: NodePath): + try: + p = NodePath('pivot') + p.reparentTo(self._holder) + np.wrtReparentTo(p) + bounds = np.getBounds() + if bounds.isEmpty(): + return + center = bounds.getApproxCenter() + radius = max(bounds.getRadius(), 0.01) + np.setPos(-center) + scale = 7.5 / max(radius, 1.0) + s = min(max(scale, 0.35), 14.0) + p.setScale(s) + except Exception: + pass + + def format_caption(self, path: Optional[str]) -> str: + if not path: + return '' + try: + base_name = os.path.basename(str(path).replace('\\', '/')) + if len(base_name) > 42: + return base_name[:39] + '…' + return base_name + except Exception: + return str(path)[:42] + + def destroy(self): + self.detach_card() + try: + self._world.removeNode() + except Exception: + pass diff --git a/toontown/toontowngui/ModernLoadingScreen.py b/toontown/toontowngui/ModernLoadingScreen.py new file mode 100644 index 0000000..72ff016 --- /dev/null +++ b/toontown/toontowngui/ModernLoadingScreen.py @@ -0,0 +1,414 @@ +import math +import time + +from panda3d.core import CardMaker, NodePath, TextNode, Vec4, ConfigVariableBool +from direct.gui.DirectGui import DirectFrame, DirectLabel, DirectWaitBar, DGG +from direct.interval.IntervalGlobal import ( + Sequence, + Parallel, + LerpColorScaleInterval, + LerpScaleInterval, + LerpFunc, + Wait, + Func, +) + +from toontown.toontowngui.LoadingAssetPreview import LoadingAssetPreview + + +class ModernLoadingScreen: + """ + Unified launcher / zone-load overlay: + - Full-screen polished startup view (logo, gradient, progress, live asset preview) + - Compact bottom bar after login (game stays visible; preview + status during loads) + """ + + def __init__(self, parent=None): + self._enabled = ConfigVariableBool('want-modern-launcher-ui', True).value + self._want_preview = ConfigVariableBool('want-loading-asset-preview', True).value + self._start_t = time.time() + self._pulse_ival = None + self._outro_ival = None + self._launcher_done = False + self._bulk_active = False + self._preview = LoadingAssetPreview() if self._enabled else None + + if not self._enabled: + self.root = None + self.compact_root = None + return + + if parent is None: + parent = aspect2d + + # Full-screen launcher panel + self.root = DirectFrame( + parent=parent, + relief=None, + frameColor=(0, 0, 0, 0), + sortOrder=100000, + ) + + self._bg = self._make_gradient_bg(self.root) + + self.logoModel = None + self.logo = None + try: + self.logoModel = loader.loadModel('phase_3/models/gui/tt_m_gui_ups_logo_noText') + if not self.logoModel or self.logoModel.isEmpty(): + raise Exception('logo model empty') + self.logo = DirectFrame( + parent=self.root, + relief=None, + image=self.logoModel, + image_scale=0.45, + pos=(0, 0, 0.58), + ) + except Exception: + self.logoModel = None + self.logo = DirectLabel( + parent=self.root, + relief=None, + text='Toontown', + text_align=TextNode.ACenter, + text_scale=0.12, + text_fg=(1, 1, 1, 1), + pos=(0, 0, 0.62), + ) + + self.subtitle = DirectLabel( + parent=self.root, + relief=None, + text='Launching…', + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(1, 1, 1, 0.85), + pos=(0, 0, 0.50), + ) + + self.status = DirectLabel( + parent=self.root, + relief=None, + text='Initializing…', + text_align=TextNode.ACenter, + text_scale=0.052, + text_fg=(1, 1, 1, 0.9), + pos=(0, 0, -0.05), + ) + + self.asset_caption = DirectLabel( + parent=self.root, + relief=None, + text='', + text_align=TextNode.ACenter, + text_scale=0.034, + text_fg=(0.75, 0.9, 1.0, 0.85), + pos=(0, 0, -0.2), + ) + + self.progress = DirectWaitBar( + parent=self.root, + relief=None, + range=100, + value=0, + pos=(0, 0, -0.16), + frameSize=(-0.75, 0.75, -0.055, 0.055), + frameColor=(1, 1, 1, 0.12), + barColor=(0.2, 0.8, 1.0, 0.85), + borderWidth=(0.02, 0.02), + ) + + self._detail = DirectLabel( + parent=self.root, + relief=None, + text='', + text_align=TextNode.ACenter, + text_scale=0.028, + text_fg=(0.55, 0.7, 0.85, 0.75), + pos=(0, 0, -0.28), + ) + + if self._want_preview and self._preview: + try: + self._preview.attach_card(self.root, (0.68, 0, 0.02), (-0.38, 0.38, -0.22, 0.22)) + except Exception: + pass + + self._start_pulse() + + # Compact in-world load bar (bottom) + self.compact_root = DirectFrame( + parent=hidden, + relief=DGG.FLAT, + frameColor=(0.03, 0.07, 0.12, 0.78), + frameSize=(-1.4, 1.4, -0.12, 0.1), + pos=(0, 0, -0.88), + sortOrder=100015, + borderWidth=(0.008, 0.008), + ) + self.compact_status = DirectLabel( + parent=self.compact_root, + relief=None, + text='', + text_align=TextNode.ALeft, + text_scale=0.04, + text_fg=(1, 1, 1, 0.95), + pos=(-1.32, 0, 0.035), + ) + self.compact_asset = DirectLabel( + parent=self.compact_root, + relief=None, + text='', + text_align=TextNode.ALeft, + text_scale=0.032, + text_fg=(0.65, 0.85, 1.0, 0.9), + pos=(-1.32, 0, -0.025), + ) + self.compact_progress = DirectWaitBar( + parent=self.compact_root, + relief=None, + range=100, + value=0, + pos=(0.15, 0, -0.055), + frameSize=(-0.55, 0.55, -0.028, 0.028), + frameColor=(1, 1, 1, 0.14), + barColor=(0.25, 0.85, 1.0, 0.9), + borderWidth=(0.015, 0.015), + ) + + self._layout_compact_bar() + + def _layout_compact_bar(self): + try: + left = float(base.a2dLeft) + 0.04 + right = float(base.a2dRight) - 0.04 + self.compact_root['frameSize'] = (left, right, -0.13, 0.1) + self.compact_status.setPos(left + 0.02, 0, 0.04) + self.compact_asset.setPos(left + 0.02, 0, -0.03) + self.compact_progress.setPos((left + right) * 0.5 + 0.1, 0, -0.055) + except Exception: + pass + + def enabled(self): + return bool(self._enabled and self.root) + + def _make_gradient_bg(self, parent) -> NodePath: + cm = CardMaker('modernLoadingBG') + cm.setFrameFullscreenQuad() + card = NodePath(cm.generate()) + card.reparentTo(parent) + card.setBin('fixed', -100) + card.setDepthTest(False) + card.setDepthWrite(False) + card.setColorScale(Vec4(0.06, 0.10, 0.16, 1.0)) + return card + + def _start_pulse(self): + if not self.root: + return + + def pulse(t): + s = 0.5 + 0.5 * math.sin(t * 2 * math.pi) + r = 0.06 + s * 0.02 + g = 0.10 + s * 0.03 + b = 0.16 + s * 0.05 + try: + self._bg.setColorScale(Vec4(r, g, b, 1.0)) + except Exception: + pass + + self._pulse_ival = Sequence( + LerpFunc(pulse, fromData=0.0, toData=1.0, duration=1.6, blendType='easeInOut'), + LerpFunc(pulse, fromData=1.0, toData=2.0, duration=1.6, blendType='easeInOut'), + ) + self._pulse_ival.loop() + + def set_title(self, title: str, subtitle=None): + if not self.root: + return + if subtitle is not None: + self.subtitle['text'] = subtitle + + def set_status(self, text: str): + if self.root: + self.status['text'] = text + if self._launcher_done and self.compact_root: + self.compact_status['text'] = text + + def set_detail(self, text: str): + if not self.root: + return + self._detail['text'] = text or '' + + def set_progress(self, pct_0_to_100: float): + try: + v = max(0.0, min(100.0, float(pct_0_to_100))) + except Exception: + v = 0.0 + if self.root: + self.progress['value'] = v + if self.compact_root: + self.compact_progress['value'] = v + + def on_asset_loaded(self, path=None, model=None, texture=None, kind='model'): + if ConfigVariableBool('want-zero-load-ui', False).value and self._launcher_done: + return + preview = self._preview if self._want_preview else None + if kind == 'audio': + if preview and path: + cap = 'SFX: ' + preview.format_caption(path) + else: + cap = path or '' + if self.root and not self._launcher_done: + self.asset_caption['text'] = cap + if self._launcher_done and self.compact_root: + self.compact_asset['text'] = cap + return + if kind == 'font': + if preview and path: + cap = 'Font: ' + preview.format_caption(path) + else: + cap = path or '' + if self.root and not self._launcher_done: + self.asset_caption['text'] = cap + if self._launcher_done and self.compact_root: + self.compact_asset['text'] = cap + return + if not preview: + return + cap = preview.format_caption(path) + if self.root and not self._launcher_done: + self.asset_caption['text'] = cap + if path: + short = path.replace('\\', '/') + if len(short) > 56: + short = short[:53] + '…' + self.set_detail(short) + if self._launcher_done and self.compact_root: + self.compact_asset['text'] = cap + try: + preview.note_asset(path, model=model, texture=texture) + except Exception: + pass + + def enter_bulk_load(self, block_name: str, label: str, expected_steps: int): + if not self._enabled: + return + self._bulk_active = True + if ConfigVariableBool('want-zero-load-ui', False).value: + return + self._layout_compact_bar() + if self._launcher_done: + try: + self.compact_root.reparentTo(aspect2d, DGG.NO_FADE_SORT_INDEX) + except Exception: + pass + self.compact_status['text'] = label + self.compact_asset['text'] = '' + self.compact_progress['value'] = 0 + if self._want_preview and self._preview: + self._preview.detach_card() + self._preview.attach_card(self.compact_root, (1.15, 0, 0.02), (-0.2, 0.2, -0.1, 0.1)) + else: + self.set_status(label) + + def leave_bulk_load(self): + self._bulk_active = False + if ConfigVariableBool('want-zero-load-ui', False).value: + if self._preview: + try: + self._preview.detach_card() + except Exception: + pass + return + if self._launcher_done and self.compact_root: + try: + self.compact_root.reparentTo(hidden) + except Exception: + pass + if self._preview: + self._preview.detach_card() + if self.root and not self._launcher_done and self._want_preview: + self._preview.attach_card(self.root, (0.68, 0, 0.02), (-0.38, 0.38, -0.22, 0.22)) + + def transition_out(self, done_cb=None): + if not self.root: + if callable(done_cb): + done_cb() + return + + if self._outro_ival: + return + + def _finish(): + try: + if self._pulse_ival: + self._pulse_ival.finish() + self._pulse_ival = None + except Exception: + pass + try: + self.root.reparentTo(hidden) + except Exception: + pass + if self._preview: + self._preview.detach_card() + self._launcher_done = True + if callable(done_cb): + done_cb() + + self._outro_ival = Sequence( + Parallel( + LerpColorScaleInterval(self.root, 0.45, Vec4(1, 1, 1, 0.0), blendType='easeInOut'), + LerpScaleInterval(self.root, 0.45, 1.06, blendType='easeInOut'), + ), + Wait(0.02), + Func(_finish), + ) + self._outro_ival.start() + + def destroy(self): + if self._pulse_ival: + try: + self._pulse_ival.finish() + except Exception: + pass + self._pulse_ival = None + + if self._outro_ival: + try: + self._outro_ival.finish() + except Exception: + pass + self._outro_ival = None + + if self._preview: + self._preview.destroy() + self._preview = None + + if self.compact_root: + try: + self.compact_root.destroy() + except Exception: + try: + self.compact_root.removeNode() + except Exception: + pass + self.compact_root = None + + if self.root: + try: + self.root.destroy() + except Exception: + try: + self.root.removeNode() + except Exception: + pass + self.root = None + + if self.logoModel: + try: + self.logoModel.removeNode() + except Exception: + pass + self.logoModel = None diff --git a/toontown/toontowngui/ToontownLoadingScreen.py b/toontown/toontowngui/ToontownLoadingScreen.py index 6973986..1b62216 100644 --- a/toontown/toontowngui/ToontownLoadingScreen.py +++ b/toontown/toontowngui/ToontownLoadingScreen.py @@ -11,53 +11,91 @@ class ToontownLoadingScreen: self.__count = 0 self.__updateSkip = 5 self.__updateCounter = 0 - self.gui = loader.loadModel('phase_3/models/gui/progress-background') + # Background art was authored for 4:3; we widen it for 16:9+ without + # stretching text/UI by keeping UI elements parented to aspect2d. + self.background = loader.loadModel('phase_3/models/gui/progress-background') self.banner = loader.loadModel('phase_3/models/gui/toon_council').find('**/scroll') - self.banner.reparentTo(self.gui) self.banner.setScale(0.4, 0.4, 0.4) self.tip = DirectLabel(guiId='ToontownLoadingScreenTip', parent=self.banner, relief=None, text='', text_scale=TTLocalizer.TLStip, textMayChange=1, pos=(-1.2, 0.0, 0.1), text_fg=(0.4, 0.3, 0.2, 1), text_wordwrap=13, text_align=TextNode.ALeft) - self.title = DirectLabel(guiId='ToontownLoadingScreenTitle', parent=self.gui, relief=None, pos=(-1.06, 0, -0.77), text='', textMayChange=1, text_scale=0.08, text_fg=(0, 0, 0.5, 1), text_align=TextNode.ALeft) - self.waitBar = DirectWaitBar(guiId='ToontownLoadingScreenWaitBar', parent=self.gui, frameSize=(-1.06, + self.title = DirectLabel(guiId='ToontownLoadingScreenTitle', parent=hidden, relief=None, pos=(-1.06, 0, -0.77), text='', textMayChange=1, text_scale=0.08, text_fg=(0, 0, 0.5, 1), text_align=TextNode.ALeft) + self.waitBar = DirectWaitBar(guiId='ToontownLoadingScreenWaitBar', parent=hidden, frameSize=(-1.06, 1.06, -0.03, 0.03), pos=(0, 0, -0.85), text='') return + def __getBackgroundXScale(self): + # aspect2d is a fixed-height space; its width expands with aspect ratio. + # The original art is laid out for 4:3 (1.333...). + try: + currentAspect = float(base.camLens.getAspectRatio()) + except Exception: + currentAspect = 4.0 / 3.0 + return max(1.0, currentAspect / (4.0 / 3.0)) + + def __applyWidescreenLayout(self): + # Keep a consistent margin from the left/right edges regardless of aspect. + leftMargin = 0.273333 # (-1.06) - (-4/3) + rightMargin = 0.273333 # (4/3) - (1.06) + xLeft = base.a2dLeft + leftMargin + xRight = base.a2dRight - rightMargin + + self.title.setPos(xLeft, 0, -0.77) + self.waitBar['frameSize'] = (xLeft, xRight, -0.03, 0.03) + self.waitBar.setPos(0, 0, -0.85) + def destroy(self): self.tip.destroy() self.title.destroy() self.waitBar.destroy() self.banner.removeNode() - self.gui.removeNode() + self.background.removeNode() def getTip(self, tipCategory): return TTLocalizer.TipTitle + '\n' + random.choice(TTLocalizer.TipDict.get(tipCategory)) - def begin(self, range, label, gui, tipCategory): + def begin(self, range, label, gui, tipCategory, minimal=False): self.waitBar['range'] = range self.title['text'] = label self.tip['text'] = self.getTip(tipCategory) self.__count = 0 self.__expectedCount = range - if gui: - self.waitBar.reparentTo(self.gui) - self.title.reparentTo(self.gui) - self.gui.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX) + if minimal: + self.waitBar.reparentTo(hidden) + self.title.reparentTo(hidden) + self.banner.reparentTo(hidden) + self.background.reparentTo(hidden) else: self.waitBar.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX) self.title.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX) - self.gui.reparentTo(hidden) + self.banner.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX) + + if gui: + self.background.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX) + self.background.setScale(self.__getBackgroundXScale(), 1.0, 1.0) + else: + self.background.reparentTo(hidden) + self.banner.reparentTo(hidden) + + self.__applyWidescreenLayout() self.waitBar.update(self.__count) + def get_progress_fraction(self): + if self.__expectedCount <= 0: + return 1.0 + return max(0.0, min(1.0, float(self.__count) / float(self.__expectedCount))) + def end(self): self.waitBar.finish() - self.waitBar.reparentTo(self.gui) - self.title.reparentTo(self.gui) - self.gui.reparentTo(hidden) + self.waitBar.reparentTo(hidden) + self.title.reparentTo(hidden) + self.banner.reparentTo(hidden) + self.background.reparentTo(hidden) return (self.__expectedCount, self.__count) def abort(self): - self.gui.reparentTo(hidden) + self.banner.reparentTo(hidden) + self.background.reparentTo(hidden) def tick(self): self.__count = self.__count + 1