"""Small AVM2 HUD renderer. Plain text only; no input, timers or model policy. The asset builder supplies constant-pool helpers. Keeping this independent of the optional asset toolchain allows source-only tests of the generated methods. """ VERSION = 3 LAYER = 'pdDebugOverlayLayer' WIDTH, HEIGHT = 276, 136 TAG_WIDTH, TAG_HEIGHT = 52, 24 LIST_WIDTH, LIST_HEIGHT = 560, 660 LIST_LINES = 29 # MODEL badge, title, two profile lines, native HP, 12 two-line rows. class Code: def __init__(self): self.raw = bytearray() self.labels = {} self.fixups = [] def add(self, data): self.raw.extend(data) return self def label(self, name): if name in self.labels: raise ValueError('duplicate AVM2 label') self.labels[name] = len(self.raw) return self def jump(self, opcode, target): self.raw.append(opcode) self.fixups.append((len(self.raw), target)) self.raw.extend(b'\0\0\0') return self def finish(self): result = bytearray(self.raw) for offset, target in self.fixups: distance = self.labels[target] - offset - 3 if not -(1 << 23) <= distance < (1 << 23): raise ValueError('AVM2 branch exceeds signed 24-bit range') result[offset:offset+3] = (distance & 0xffffff).to_bytes(3, 'little') return bytes(result) def methods(op, qname, string, uint): """(name, bytecode, result type, parameters, getter, extra locals).""" layer = qname(LAYER) getlayer = b'\xd0' + op(0x66, layer) stage = b'\xd0' + op(0x66, qname('stage')) sprite = qname('Sprite', 'flash.display') text = lambda s: op(0x2c, string(s)) number = lambda n: op(0x25, n) local = lambda n: op(0x62, n) store = lambda n: op(0x63, n) prop = lambda obj, key, value: obj + value + op(0x61, qname(key)) invoke = lambda obj, key, *args: obj + b''.join(args) + op(0x46, qname(key), len(args)) new_sprite = op(0x60, sprite) + op(0x42, 0) # 1..9 are arguments; 10=card, 11=text field, 12=graphics. # IDs >= 10 are compact world tags; detail cards never draw world leaders. # Separate methods share the implementation but create distinct child names. def renderer(tag): width, height = (TAG_WIDTH, TAG_HEIGHT) if tag else (WIDTH, HEIGHT) code = Code().add(stage).jump(0x12, 'unavailable') code.add(getlayer).jump(0x11, 'layer_ready') code.add(b'\xd0' + new_sprite + op(0x61, layer)) for key in ('mouseEnabled', 'mouseChildren', 'tabChildren'): code.add(prop(getlayer, key, b'\x27')) code.add(prop(getlayer, 'name', text('PathDevkitDebugOverlay'))) code.add(invoke(stage, 'addChild', getlayer) + b'\x29') code.label('layer_ready') code.add(invoke(getlayer, 'getChildByName', text('card') + b'\xd1\x70\xa0') + store(10)) code.add(local(10)).jump(0x11, 'card_ready') code.add(new_sprite + store(10)) code.add(prop(local(10), 'name', text('card') + b'\xd1\x70\xa0')) for key in ('mouseEnabled', 'mouseChildren', 'tabChildren'): code.add(prop(local(10), key, b'\x27')) fields = [('title', 0, 24, True, 0xffffff)] if tag else [ ('title', 7, 25, True, 0xffffff), *[(f'body{i}', 35 + i * 22, 24, False, 0xd8e2eb) for i in range(4)]] for name, y, field_height, bold, color in fields: code.add(invoke(op(0x60, qname('CommonUtils', 'red.game.witcher3.utils')), 'spawnTextField', number(16 if tag or not bold else 18), op(0x2e, uint(color)), b'\x27') + store(11)) # The factory's third argument enables its debug background, not bold. code.add(local(11) + op(0x66, qname('defaultTextFormat')) + store(12)) code.add(prop(local(12), 'bold', b'\x26' if bold else b'\x27')) code.add(prop(local(12), 'align', text('center' if tag else 'left'))) code.add(prop(local(12), 'leading', number(3))) code.add(prop(local(11), 'defaultTextFormat', local(12))) code.add(invoke(local(11), 'setTextFormat', local(12)) + b'\x29') for key, value in (('name', text(name)), ('x', number(0 if tag else 11)), ('y', number(y)), ('width', number(width if tag else width-20)), ('height', number(field_height)), ('multiline', b'\x27'), ('wordWrap', b'\x27'), ('background', b'\x27'), ('border', b'\x27'), ('selectable', b'\x27'), ('mouseEnabled', b'\x27'), ('tabEnabled', b'\x27')): code.add(prop(local(11), key, value)) code.add(invoke(local(10), 'addChild', local(11)) + b'\x29') code.add(invoke(getlayer, 'addChild', local(10)) + b'\x29') code.label('card_ready') code.add(prop(local(10), 'x', local(4))) code.add(prop(local(10), 'y', local(5))) code.add(prop(local(10), 'visible', local(7))) code.add(invoke(b'\xd0', 'pdOverlayText', invoke(local(10), 'getChildByName', text('title')), local(2), number(4 if tag else 20)) + b'\x29') if not tag: # Padding makes every indexed line a String, including absent rows. code.add(invoke(local(3) + text('\n\n\n') + b'\xa0', 'split', text('\n')) + store(12)) for i in range(4): code.add(invoke(b'\xd0', 'pdOverlayText', invoke(local(10), 'getChildByName', text(f'body{i}')), local(12) + op(0x66, qname(str(i))), number(24)) + b'\x29') code.add(local(10) + op(0x66, qname('graphics')) + store(12)) graphics = local(12) code.add(invoke(graphics, 'clear') + b'\x29') code.add(invoke(graphics, 'lineStyle', number(1), local(6), number(1)) + b'\x29') code.add(invoke(graphics, 'beginFill', op(0x2e, uint(0x101820)), number(9) + number(10) + b'\xa3') + b'\x29') code.add(invoke(graphics, 'drawRoundRect', number(0), number(0), number(width), number(height), number(6), number(6)) + b'\x29') code.add(invoke(graphics, 'endFill') + b'\x29') if not tag: code.add(invoke(graphics, 'lineStyle', number(0), local(6), number(0)) + b'\x29') code.add(invoke(graphics, 'beginFill', local(6), number(1)) + b'\x29') code.add(invoke(graphics, 'drawRect', number(0), number(0), number(3), number(height)) + b'\x29') code.add(invoke(graphics, 'endFill') + b'\x29') code.add(b'\x26\x48') code.label('unavailable').add(b'\x27') return code.finish() # Use the same constant-index field generation as simple cards. Every list # character was validated/split by the bridge; never use the ellipsis helper. listing = Code().add(stage).jump(0x12, 'unavailable') listing.add(getlayer).jump(0x12, 'unavailable') # Simple cards create our layer first. listing.add(invoke(getlayer, 'getChildByName', text('list')) + store(10)) listing.add(local(10)).jump(0x11, 'list_ready') listing.add(new_sprite + store(10)) listing.add(prop(local(10), 'name', text('list'))) for key in ('mouseEnabled', 'mouseChildren', 'tabChildren'): listing.add(prop(local(10), key, b'\x27')) listing.add(invoke(getlayer, 'addChild', local(10)) + b'\x29') listing.label('list_ready') listing.add(prop(local(10), 'x', local(4))) listing.add(prop(local(10), 'y', local(5))) listing.add(prop(local(10), 'visible', local(7))) listing.add(invoke(local(3) + text('\n' * LIST_LINES) + b'\xa0', 'split', text('\n')) + store(13)) for i in range(LIST_LINES): listing.add(invoke(local(10), 'getChildByName', text(f'line{i}')) + store(11)) listing.add(local(11)).jump(0x11, f'field_ready_{i}') listing.add(invoke(op(0x60, qname('CommonUtils', 'red.game.witcher3.utils')), 'spawnTextField', number(16), op(0x2e, uint(0xd8e2eb)), b'\x27') + store(11)) listing.add(local(11) + op(0x66, qname('defaultTextFormat')) + store(12)) listing.add(prop(local(12), 'bold', b'\x26' if i < 2 else b'\x27')) listing.add(prop(local(12), 'align', text('left'))) listing.add(prop(local(12), 'leading', number(3))) listing.add(prop(local(11), 'defaultTextFormat', local(12))) listing.add(invoke(local(11), 'setTextFormat', local(12)) + b'\x29') for key, value in ( ('name', text(f'line{i}')), ('x', number(12)), ('y', number(8 + i * 22)), ('width', number(LIST_WIDTH - 24)), ('height', number(24)), ('multiline', b'\x27'), ('wordWrap', b'\x27'), ('background', b'\x27'), ('border', b'\x27'), ('selectable', b'\x27'), ('mouseEnabled', b'\x27'), ('tabEnabled', b'\x27')): listing.add(prop(local(11), key, value)) listing.add(invoke(local(10), 'addChild', local(11)) + b'\x29') listing.label(f'field_ready_{i}') listing.add(prop(local(11), 'text', local(13) + op(0x66, qname(str(i))))) listing.add(local(10) + op(0x66, qname('graphics')) + store(12)) graphics = local(12) listing.add(invoke(graphics, 'clear') + b'\x29') listing.add(invoke(graphics, 'lineStyle', number(1), local(6), number(1)) + b'\x29') listing.add(invoke(graphics, 'beginFill', op(0x2e, uint(0x101820)), number(9) + number(10) + b'\xa3') + b'\x29') listing.add(invoke(graphics, 'drawRoundRect', number(0), number(0), number(LIST_WIDTH), number(LIST_HEIGHT), number(6), number(6)) + b'\x29') listing.add(invoke(graphics, 'endFill') + b'\x29') listing.add(invoke(graphics, 'lineStyle', number(0), local(6), number(0)) + b'\x29') listing.add(invoke(graphics, 'beginFill', op(0x2e, uint(0x52616a)), number(1)) + b'\x29') for i in range(12): listing.add(invoke(graphics, 'drawRect', number(12), number(116 + i * 44), number(LIST_WIDTH - 24), number(1)) + b'\x29') listing.add(invoke(graphics, 'endFill') + b'\x29\x26\x48') listing.label('unavailable').add(b'\x27') clear = Code().add(getlayer).jump(0x12, 'done') clear.add(getlayer + op(0x66, qname('parent'))).jump(0x12, 'forget') clear.add(invoke(getlayer + op(0x66, qname('parent')), 'removeChild', getlayer) + b'\x29') clear.label('forget').add(b'\xd0\x20' + op(0x61, layer)) clear.label('done').add(b'\x26') visible = Code().add(getlayer).jump(0x12, 'absent') visible.add(prop(getlayer, 'visible', b'\xd1') + b'\x26\x48') visible.label('absent').add(b'\x27') attached = Code().add(getlayer).jump(0x12, 'absent') attached.add(getlayer + op(0x66, qname('parent')) + stage + b'\xac\x48') attached.label('absent').add(b'\x27') # Same-update textWidth was unreliable in the game's GFx runtime. Use a # conservative visual budget, including prefixes and the ellipsis. The # larger bold titles use 20 characters; smaller body rows use 24. # Full strings remain in publication status. fit = Code().add(b'\xd2' + op(0x66, qname('length')) + b'\xd3').jump(0x16, 'ready') fit.add(invoke(b'\xd2', 'substr', number(0), b'\xd3' + number(3) + b'\xa1') + text('...') + b'\xa0\xd6') fit.label('ready').add(prop(b'\xd1', 'text', b'\xd2') + b'\x26') return [ ('pdOverlayText', fit.finish(), 'Boolean', ('Object','String','int'), False, 0), ('pdOverlayCard', renderer(False), 'Boolean', ('int','String','String','Number','Number','uint','Boolean','Number','Number'), False, 3), ('pdOverlayTag', renderer(True), 'Boolean', ('int','String','String','Number','Number','uint','Boolean','Number','Number'), False, 3), ('pdOverlayList', listing.finish(), 'Boolean', ('int','String','String','Number','Number','uint','Boolean','Number','Number'), False, 4), ('pdOverlayClear', clear.finish(), 'Boolean', (), False, 0), ('pdOverlayVisible', visible.finish(), 'Boolean', ('Boolean',), False, 0), ('pdOverlayAttached', attached.finish(), 'Boolean', (), True, 0), ('pdOverlayVersion', number(VERSION), 'int', (), True, 0), ]