UI: Add full modern loading system with 3D asset preview and zone prefetching

This commit is contained in:
Toontown Super 2026-08-02 02:46:59 -04:00
parent c2b808182d
commit b41c22cb68
6 changed files with 826 additions and 28 deletions

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()