2626 SoundSpec ,
2727 CollisionMeshSpec ,
2828 AssetBucketKind ,
29+ AssetIndexContext ,
2930 )
31+ from bacommon .langstr import LangStrFlatIndexContext
3032 import bacommon .clienteffect as clfx
3133
3234
@@ -166,8 +168,18 @@ def _deindex_effects(effects: 'list[clfx.Effect]') -> None:
166168 if response .packages :
167169 # Strings first: the effect de-index below consumes them, and
168170 # it expects the two-int form rather than a folded index.
169- deindex_langstrs (response .page , packages , response .client_effects )
170- deindex_assets (response .page , packages , response .client_effects )
171+ deindex_langstrs (
172+ response .page ,
173+ packages ,
174+ response .client_effects ,
175+ expect_digest = response .langstr_index_digest ,
176+ )
177+ deindex_assets (
178+ response .page ,
179+ packages ,
180+ response .client_effects ,
181+ expect_digest = response .asset_index_digest ,
182+ )
171183 _deindex_effects (response .client_effects )
172184 for row in response .page .rows :
173185 if not isinstance (row , dui2 .ButtonRow ):
@@ -181,11 +193,17 @@ def package_asset_listing(apverid: str) -> list[str] | None:
181193 """Canonical sorted logical paths for every asset in a package.
182194
183195 The client's half of the flat-index mapping. Deliberately the union
184- across buckets rather than one bucket: the server derives its
185- listing from a vendored wrapper tree that groups paths differently
186- (collision meshes sit in the ``constant`` bucket here but under
187- ``meshes`` there), so only the union is guaranteed to agree. Indexing
188- the whole package makes the grouping irrelevant.
196+ across buckets rather than one bucket: the server groups the same
197+ paths differently (collision meshes sit in the ``constant`` bucket
198+ here but under ``meshes`` there), so only the union is guaranteed
199+ to agree. Indexing the whole package makes the grouping irrelevant.
200+
201+ This is the *registry*, i.e. what the package actually built, which
202+ is why the server has to vendor an equally complete listing rather
203+ than derive one from its wrapper accessors -- cube maps and the
204+ legacy-language-data blob appear here and have no accessor. When
205+ the two lists disagree the digest check in :func:`_domains_agree`
206+ is what catches it; nothing else would.
189207
190208 ``None`` when the package isn't registered at all -- a
191209 resolve-ordering fault -- as distinct from a registered package that
@@ -224,6 +242,8 @@ def deindex_langstrs(
224242 page : dui2 .Page ,
225243 packages : list [str ],
226244 effects : 'list[clfx.Effect] | None' = None ,
245+ * ,
246+ expect_digest : str | None = None ,
227247) -> None :
228248 """Unfold a page's flat string indices into the two-int form.
229249
@@ -233,6 +253,11 @@ def deindex_langstrs(
233253 what the native decoder already consumed before folding existed --
234254 so nothing downstream changes.
235255
256+ ``expect_digest`` is the producer's
257+ :meth:`~bacommon.langstr.LangStrFlatIndexContext.domain_digest`;
258+ see :func:`deindex_assets` for why a mismatch means we must not
259+ unfold at all.
260+
236261 Fail-visible per slot: a bad index logs and leaves the integer in
237262 place rather than substituting a wrong string.
238263 """
@@ -248,6 +273,8 @@ def deindex_langstrs(
248273 return
249274
250275 ctx = LangStrFlatIndexContext (packages , package_string_count )
276+ if not _domains_agree (ctx , expect_digest , 'language-string' ):
277+ return
251278
252279 def _lstr (val : 'LangStrSpec | int' ) -> 'LangStrSpec | None' :
253280 # Spec-form strings pass through: a response can mix forms, and
@@ -270,10 +297,51 @@ def _lstr(val: 'LangStrSpec | int') -> 'LangStrSpec | None':
270297 clfx .walk_effects (effects , langstr = _lstr )
271298
272299
300+ def _domains_agree (
301+ ctx : 'AssetIndexContext | LangStrFlatIndexContext' ,
302+ expect_digest : str | None ,
303+ what : str ,
304+ ) -> bool :
305+ """Whether an index domain matches the one the producer used.
306+
307+ The two ends build their domains from different sources, so they
308+ can disagree -- and a disagreement hides itself: an index that is
309+ wrong but still in range names a different asset or string, so the
310+ page renders wrong and nothing raises. The digest is the only thing
311+ that makes that visible, which is why a mismatch has to stop the
312+ de-index entirely rather than proceed per-slot. Leaving the
313+ integers in place lands the failure on the existing loud paths.
314+
315+ A payload with no digest (an older producer) is taken as agreeing;
316+ the digest is a guard, not a requirement.
317+ """
318+ import bauiv1 as bui
319+
320+ if expect_digest is None :
321+ return True
322+ ours = ctx .domain_digest ()
323+ if ours == expect_digest :
324+ return True
325+ bui .uilog .error (
326+ 'Doc-ui %s index domain disagrees with the producer'
327+ ' (ours %s, theirs %s); refusing to de-index, so this page will'
328+ ' render incompletely. Our per-package sizes: %s. This means the'
329+ ' two ends derive different listings for some package above;'
330+ ' compare those sizes against the producer side.' ,
331+ what ,
332+ ours ,
333+ expect_digest ,
334+ ctx .describe_domain (),
335+ )
336+ return False
337+
338+
273339def deindex_assets (
274340 page : dui2 .Page ,
275341 packages : list [str ],
276342 effects : 'list[clfx.Effect] | None' = None ,
343+ * ,
344+ expect_digest : str | None = None ,
277345) -> None :
278346 """Replace a page's flat asset indices with real specs.
279347
@@ -282,27 +350,25 @@ def deindex_assets(
282350 -- prep, render, the depiction code -- therefore only ever sees
283351 specs, and never has to know the indexed form existed.
284352
353+ ``expect_digest`` is the producer's
354+ :meth:`~bacommon.assetspec.AssetIndexContext.domain_digest`; a
355+ mismatch skips de-indexing entirely (see :func:`_domains_agree`).
356+
285357 Fail-visible per reference: a bad index logs and leaves the integer
286358 in place rather than substituting a wrong asset, and the render then
287359 fails on that one slot.
288360 """
289361 import bauiv1 as bui
290362
291- # Runtime imports: the narrowing below is real isinstance work, not
292- # just annotation.
293- from bacommon .assetspec import (
294- AssetIndexContext ,
295- AssetIndexError ,
296- TextureSpec ,
297- MeshSpec ,
298- SoundSpec ,
299- )
363+ from bacommon .assetspec import AssetIndexContext , AssetIndexError
300364 from bacommon .docui .walk import walk_page
301365
302366 if not packages :
303367 return
304368
305369 ctx = AssetIndexContext (packages , package_asset_listing )
370+ if not _domains_agree (ctx , expect_digest , 'asset' ):
371+ return
306372
307373 def _convert (
308374 ref : 'TextureSpec | MeshSpec | SoundSpec | int' ,
@@ -320,33 +386,20 @@ def _convert(
320386 )
321387 return None
322388
323- # Two thin wrappers rather than one visitor: each walk's slots hold
324- # a different spec union, and a visitor's *return* has to fit the
325- # slot it is written back into.
326- def _pageref (
327- ref : 'TextureSpec | MeshSpec | int' , kind : 'AssetBucketKind'
328- ) -> 'TextureSpec | MeshSpec | int | None' :
329- out = _convert (ref , kind )
330- # Page slots hold textures and meshes only; anything else means
331- # the slot's kind and the domain disagreed, which should fail
332- # loudly rather than write a wrong type into the page.
333- if out is None or isinstance (out , (TextureSpec , MeshSpec )):
334- return out
335- return None
336-
337- def _effectref (
338- ref : 'SoundSpec | int' , kind : 'AssetBucketKind'
339- ) -> 'SoundSpec | int | None' :
340- out = _convert (ref , kind )
341- return out if isinstance (out , SoundSpec ) else None
342-
343- walk_page (page , assetref = _pageref )
389+ # One visitor for every slot, not one per spec union. `walk_page`
390+ # hands a button's immediate client-effects to `walk_effects` using
391+ # the *page* visitor, so a textures-and-meshes-only visitor silently
392+ # dropped the sound refs in them -- they stayed integers and failed
393+ # at run time with 'Un-de-indexed sound ref N in a client-effect'.
394+ # The slot's own `kind` already decides which spec `_convert`
395+ # produces, so no narrowing is needed (or correct) here.
396+ walk_page (page , assetref = _convert ) # type: ignore[arg-type]
344397 if effects :
345398 # Response-level effects are outside the page walk, and their
346399 # sounds are indexed the same as page art.
347400 import bacommon .clienteffect as clfx
348401
349- clfx .walk_effects (effects , assetref = _effectref )
402+ clfx .walk_effects (effects , assetref = _convert ) # type: ignore[arg-type]
350403
351404
352405def _resolve_packages_blocking (apverids : list [str ], locale : Locale ) -> None :
0 commit comments