From b41c22cb681cc4d27ee5ee93fb5c6cb59bbf07b8 Mon Sep 17 00:00:00 2001 From: Toontown Super <108632596+ToontownSuperForeverMVP@users.noreply.github.com> Date: Sun, 2 Aug 2026 02:46:59 -0400 Subject: [PATCH] UI: Add full modern loading system with 3D asset preview and zone prefetching --- toontown/toonbase/AssetCache.py | 24 +- toontown/toonbase/ToontownLoader.py | 33 +- toontown/toontowngui/LoadingAssetPreview.py | 223 ++++++++++ toontown/toontowngui/ModernLoadingScreen.py | 414 ++++++++++++++++++ toontown/toontowngui/ToontownLoadingScreen.py | 56 ++- toontown/toontowngui/ZonePrefetchCatalog.py | 104 +++++ 6 files changed, 826 insertions(+), 28 deletions(-) create mode 100644 toontown/toontowngui/LoadingAssetPreview.py create mode 100644 toontown/toontowngui/ModernLoadingScreen.py create mode 100644 toontown/toontowngui/ZonePrefetchCatalog.py diff --git a/toontown/toonbase/AssetCache.py b/toontown/toonbase/AssetCache.py index ef0c021..833b8da 100644 --- a/toontown/toonbase/AssetCache.py +++ b/toontown/toonbase/AssetCache.py @@ -1,6 +1,3 @@ -""" -AssetCache - Preloads and caches common assets for instant zone loading -""" from direct.directnotify.DirectNotifyGlobal import directNotify from panda3d.core import ModelPool, TexturePool, NodePath @@ -23,42 +20,40 @@ class AssetCache: return cls._instance def initialize(self, loader): - """Preload common assets used across all zones""" if self._initialized: return self.notify.info('Preloading common assets for faster zone loading...') - # Common models that are used everywhere commonModels = [ 'phase_3/models/gui/toontown-logo', + 'phase_3/models/gui/tt_m_gui_ups_logo_noText', + 'phase_3/models/gui/progress-background', + 'phase_3/models/gui/dialog_box_gui', + 'phase_3/models/gui/quit_button', 'phase_3/models/props/arrow', 'phase_3/models/props/panel', 'phase_3/models/props/chatbox', 'phase_3/models/props/chatbox_noarrow', 'phase_3/models/gui/chat_button_gui', 'phase_3/models/misc/sphere', + 'phase_3.5/models/props/drop_shadow', + 'phase_3.5/models/gui/inventory_icons', ] - # Preload models asynchronously for modelPath in commonModels: try: - model = loader.loadModel(modelPath) + model = loader.loadModel(modelPath, okMissing=True) if model: self.preloadedModels[modelPath] = model - # Keep in model pool ModelPool.addModel(modelPath, model.node()) - except: + except Exception: pass - # Texture and Model pools manage their own sizes automatically - # Just ensure they're enabled for caching - self._initialized = True self.notify.info('Asset preloading complete') def cleanup(self): - """Clean up cached assets""" self.preloadedModels.clear() self.preloadedTextures.clear() self.preloadedSounds.clear() @@ -66,10 +61,7 @@ class AssetCache: self._initialized = False def getModel(self, modelPath): - """Get a preloaded model if available""" return self.preloadedModels.get(modelPath) -# Global instance assetCache = AssetCache.getInstance() - diff --git a/toontown/toonbase/ToontownLoader.py b/toontown/toonbase/ToontownLoader.py index a23a0c8..caf8741 100644 --- a/toontown/toonbase/ToontownLoader.py +++ b/toontown/toonbase/ToontownLoader.py @@ -3,9 +3,10 @@ from panda3d.toontown import * from direct.directnotify.DirectNotifyGlobal import * from direct.showbase import Loader from toontown.toontowngui import ToontownLoadingScreen +from toontown.toontowngui.ZonePrefetchCatalog import zonePrefetchManager class ToontownLoader(Loader.Loader): - TickPeriod = 0.01 + TickPeriod = 0.005 def __init__(self, base): Loader.Loader.__init__(self, base) @@ -13,12 +14,15 @@ class ToontownLoader(Loader.Loader): self.blockName = None self.loadingScreen = ToontownLoadingScreen.ToontownLoadingScreen() self._tickCounter = 0 - self._tickSkip = 10 + self._tickSkip = 1 + self._lastTickT = 0.0 + self._loadStartT = 0.0 return def destroy(self): - self.loadingScreen.destroy() - del self.loadingScreen + if hasattr(self, 'loadingScreen') and self.loadingScreen: + self.loadingScreen.destroy() + del self.loadingScreen Loader.Loader.destroy(self) def beginBulkLoad(self, name, label, range, gui, tipCategory): @@ -31,6 +35,11 @@ class ToontownLoader(Loader.Loader): self._lastTickT = globalClock.getRealTime() self.blockName = name self.loadingScreen.begin(range, label, gui, tipCategory) + try: + if isinstance(name, int) or (isinstance(name, str) and name.isdigit()): + zonePrefetchManager.prefetch_zone(int(name), loader=self) + except Exception: + pass return None def endBulkLoad(self, name): @@ -63,21 +72,27 @@ class ToontownLoader(Loader.Loader): self._tickCounter = 0 now = globalClock.getRealTime() if now - self._lastTickT > self.TickPeriod: - self._lastTickT += self.TickPeriod + self._lastTickT = now self.loadingScreen.tick() try: base.cr.considerHeartbeat() - except: + except Exception: pass def loadModel(self, *args, **kw): ret = Loader.Loader.loadModel(self, *args, **kw) self.tick() + if ret and hasattr(self.loadingScreen, 'on_asset_loaded'): + path = args[0] if args else None + self.loadingScreen.on_asset_loaded(path=path, model=ret, kind='model') return ret def loadFont(self, *args, **kw): ret = Loader.Loader.loadFont(self, *args, **kw) self.tick() + if ret and hasattr(self.loadingScreen, 'on_asset_loaded'): + path = args[0] if args else None + self.loadingScreen.on_asset_loaded(path=path, kind='font') return ret def loadTexture(self, texturePath, alphaPath = None, okMissing = False): @@ -85,16 +100,22 @@ class ToontownLoader(Loader.Loader): self.tick() if alphaPath: self.tick() + if ret and hasattr(self.loadingScreen, 'on_asset_loaded'): + self.loadingScreen.on_asset_loaded(path=texturePath, texture=ret, kind='texture') return ret def loadSfx(self, soundPath): ret = Loader.Loader.loadSfx(self, soundPath) self.tick() + if ret and hasattr(self.loadingScreen, 'on_asset_loaded'): + self.loadingScreen.on_asset_loaded(path=soundPath, kind='audio') return ret def loadMusic(self, soundPath): ret = Loader.Loader.loadMusic(self, soundPath) self.tick() + if ret and hasattr(self.loadingScreen, 'on_asset_loaded'): + self.loadingScreen.on_asset_loaded(path=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..6d1c89e 100644 --- a/toontown/toontowngui/ToontownLoadingScreen.py +++ b/toontown/toontowngui/ToontownLoadingScreen.py @@ -3,14 +3,21 @@ from panda3d.core import * from toontown.toonbase import ToontownGlobals from toontown.toonbase import TTLocalizer import random +from toontown.toontowngui import ModernLoadingScreen class ToontownLoadingScreen: def __init__(self): self.__expectedCount = 0 self.__count = 0 - self.__updateSkip = 5 + self.__updateSkip = 1 self.__updateCounter = 0 + self.modern = None + if ConfigVariableBool('want-modern-launcher-ui', True).value: + try: + self.modern = ModernLoadingScreen.ModernLoadingScreen() + except Exception: + self.modern = None self.gui = 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) @@ -24,6 +31,12 @@ class ToontownLoadingScreen: return def destroy(self): + if self.modern: + try: + self.modern.destroy() + except Exception: + pass + self.modern = None self.tip.destroy() self.title.destroy() self.waitBar.destroy() @@ -31,14 +44,27 @@ class ToontownLoadingScreen: self.gui.removeNode() def getTip(self, tipCategory): - return TTLocalizer.TipTitle + '\n' + random.choice(TTLocalizer.TipDict.get(tipCategory)) + tips = TTLocalizer.TipDict.get(tipCategory) + if tips: + return TTLocalizer.TipTitle + '\n' + random.choice(tips) + return TTLocalizer.TipTitle def begin(self, range, label, gui, tipCategory): + self.__count = 0 + self.__expectedCount = max(1, range) + tip_text = self.getTip(tipCategory) + if self.modern and self.modern.enabled(): + self.modern.enter_bulk_load('bulk', label, self.__expectedCount) + self.modern.set_status(label) + self.modern.set_detail(tip_text) + self.modern.set_progress(0.0) + self.gui.reparentTo(hidden) + self.waitBar.reparentTo(hidden) + self.title.reparentTo(hidden) + return self.waitBar['range'] = range self.title['text'] = label - self.tip['text'] = self.getTip(tipCategory) - self.__count = 0 - self.__expectedCount = range + self.tip['text'] = tip_text if gui: self.waitBar.reparentTo(self.gui) self.title.reparentTo(self.gui) @@ -50,6 +76,10 @@ class ToontownLoadingScreen: self.waitBar.update(self.__count) def end(self): + if self.modern and self.modern.enabled(): + self.modern.set_progress(100.0) + self.modern.leave_bulk_load() + return (self.__expectedCount, self.__count) self.waitBar.finish() self.waitBar.reparentTo(self.gui) self.title.reparentTo(self.gui) @@ -57,11 +87,25 @@ class ToontownLoadingScreen: return (self.__expectedCount, self.__count) def abort(self): + if self.modern and self.modern.enabled(): + self.modern.leave_bulk_load() + return self.gui.reparentTo(hidden) def tick(self): - self.__count = self.__count + 1 + self.__count += 1 + if self.modern and self.modern.enabled(): + pct = (float(self.__count) / float(self.__expectedCount)) * 100.0 + self.modern.set_progress(pct) + return self.__updateCounter += 1 if self.__updateCounter >= self.__updateSkip: self.__updateCounter = 0 self.waitBar.update(self.__count) + + def on_asset_loaded(self, path=None, model=None, texture=None, kind='model'): + if self.modern and self.modern.enabled(): + try: + self.modern.on_asset_loaded(path=path, model=model, texture=texture, kind=kind) + except Exception: + pass diff --git a/toontown/toontowngui/ZonePrefetchCatalog.py b/toontown/toontowngui/ZonePrefetchCatalog.py new file mode 100644 index 0000000..240ee52 --- /dev/null +++ b/toontown/toontowngui/ZonePrefetchCatalog.py @@ -0,0 +1,104 @@ +from panda3d.core import ModelPool, TexturePool, ConfigVariableBool +from direct.directnotify.DirectNotifyGlobal import directNotify +from toontown.toonbase import ToontownGlobals + +notify = directNotify.newCategory('ZonePrefetchCatalog') + +ZONE_ASSETS_MAP = { + 2000: [ + 'phase_4/models/neighborhoods/toontown_central', + 'phase_3.5/models/modules/skys/TT_sky', + 'phase_4/models/props/tt_m_prp_ext_fountain', + 'phase_3.5/models/props/tunnel_sign_toontown', + ], + 1000: [ + 'phase_6/models/neighborhoods/donalds_dock', + 'phase_3.5/models/modules/skys/cloud_sky', + 'phase_6/models/props/dock_boat', + ], + 3000: [ + 'phase_6/models/neighborhoods/minnies_melody_land', + 'phase_6/models/modules/skys/mml_sky', + ], + 4000: [ + 'phase_8/models/neighborhoods/daisys_garden', + 'phase_3.5/models/modules/skys/TT_sky', + ], + 5000: [ + 'phase_6/models/neighborhoods/the_brrrgh', + 'phase_6/models/modules/skys/brrrgh_sky', + ], + 9000: [ + 'phase_8/models/neighborhoods/donalds_dreamland', + 'phase_8/models/modules/skys/night_sky', + ], + 8000: [ + 'phase_6/models/karting/speedway_hub', + 'phase_3.5/models/modules/skys/TT_sky', + ], + 6000: [ + 'phase_6/models/golf/chip_n_dales_acorn_acres', + ], + 11000: [ + 'phase_9/models/cogHQ/SellbotHQ_outer', + 'phase_9/models/cogHQ/SellbotHQ_inner', + ], + 12000: [ + 'phase_10/models/cogHQ/CashbotHQExterior', + ], + 13000: [ + 'phase_11/models/lawbotHQ/LawbotHQExterior', + ], + 10000: [ + 'phase_12/models/bossbotHQ/BossbotHQExterior', + ], + 500: [ + 'phase_5.5/models/estate/estate_house', + ], +} + +class ZonePrefetchManager: + def __init__(self): + self._prefetched_zones = set() + self._cached_models = {} + self._enabled = ConfigVariableBool('want-async-zone-prefetch', True).value + + def prefetch_zone(self, zoneId, loader=None): + if not self._enabled: + return + base_zone = (zoneId // 1000) * 1000 + if base_zone == 0: + base_zone = zoneId + if base_zone in self._prefetched_zones: + return + assets = ZONE_ASSETS_MAP.get(base_zone) or ZONE_ASSETS_MAP.get(zoneId) + if not assets: + return + if loader is None: + try: + loader = base.loader + except Exception: + return + self._prefetched_zones.add(base_zone) + notify.info('Prefetching zone %s assets (%d models)...' % (zoneId, len(assets))) + for path in assets: + if path in self._cached_models: + continue + try: + if hasattr(loader, 'loadModel'): + model = loader.loadModel(path, okMissing=True) + if model and not model.isEmpty(): + self._cached_models[path] = model + ModelPool.addModel(path, model.node()) + except Exception as e: + notify.debug('Prefetch failed for %s: %s' % (path, e)) + + def is_zone_prefetched(self, zoneId): + base_zone = (zoneId // 1000) * 1000 + return (base_zone in self._prefetched_zones) or (zoneId in self._prefetched_zones) + + def clear_cache(self): + self._cached_models.clear() + self._prefetched_zones.clear() + +zonePrefetchManager = ZonePrefetchManager()