Created
August 9, 2026 20:07
-
-
Save bee-san/c493971eda6c02082d82c84511bb919d to your computer and use it in GitHub Desktop.
persona mod
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| These are published in p3rpc.nativetypes/Xrd777.cs and the message structures later in that file. | |
| So P3R already maintains: | |
| The current visible page. | |
| Its lines and individual UTF-8 characters. | |
| A separate color for every character. | |
| That last field explains the yellow highlight: change the selected letters’ FSprColor, let P3R render its normal frame, then restore the colors. | |
| Where the coordinates come from | |
| P3R has this reverse-engineered native function: | |
| void UMsgProcWindow_Simple_DrawMessageText( | |
| UMsgProcWindow_Simple* self, | |
| nint masker, | |
| byte opacity, | |
| float posX, | |
| float posY); | |
| It receives the message origin directly. See IUIMethods.cs. | |
| A current signature for that function is: | |
| 4C 8B DC 49 89 5B ?? 57 48 81 EC D0 00 00 00 | |
| 48 8B 81 ?? ?? ?? ?? | |
| The open-source FeMC mod demonstrates signature-scanning it. Its replacement message-box renderer computes: | |
| msgBaseX = self->OffsetX + 482; | |
| msgBaseY = 883; | |
| DrawMessageText(self, masker, opacity, msgBaseX, msgBaseY); | |
| See MsgWindowSimple.cs. P3R UI uses a 1920×1080 logical canvas; the same implementation visibly passes 1920, 1080 to its mask renderer. | |
| Therefore the chain is: | |
| AOB signature → live draw function | |
| → self/current BMD page | |
| → real text and per-letter colors | |
| → posX/posY message origin | |
| posX/posY is the message origin, not necessarily each glyph’s final rectangle. For that, hook the lower line/glyph renderer. | |
| Exact recreation path | |
| 1. Make a Reloaded-II P3R mod | |
| Use C# with unsafe code, Reloaded.Hooks, and p3rpc.nativetypes.Interfaces. | |
| Signature-scan P3R.exe; never ship absolute addresses. Require exactly one match and disable the feature if validation fails. | |
| 2. Detour DrawMessageText | |
| The core lifecycle looks like this: | |
| unsafe void DrawMessageTextDetour( | |
| UMsgProcWindow_Simple* self, | |
| nint masker, | |
| byte opacity, | |
| float x, | |
| float y) | |
| { | |
| var work = ((UMsgProcWindowBase*)self)->pMsgWork; | |
| var page = work == null ? null : work->CurrentPage; | |
| BeginMessageCapture(page, x, y); | |
| var changed = ApplyHoveredColors(page); | |
| try | |
| { | |
| _drawMessageTextHook.OriginalFunction( | |
| self, masker, opacity, x, y); | |
| } | |
| finally | |
| { | |
| RestoreColors(changed); | |
| EndMessageCapture(); | |
| } | |
| } | |
| Never retain letter pointers after CurrentPage changes. | |
| 3. Traverse the page carefully | |
| CharacterData appears to be a list sentinel. Treat both lists as potentially circular: | |
| var sentinel = page->CharacterData; | |
| var line = sentinel->Next; | |
| for (int li = 0; | |
| line != null && line != sentinel && li < page->LineCount; | |
| li++, line = line->Next) | |
| { | |
| var letter = line->FirstCharacter; | |
| for (ulong i = 0; | |
| letter != null && i < line->LetterCount; | |
| i++) | |
| { | |
| DecodeOneUtf8Rune((byte*)letter); | |
| SaveNodeForRune(letter); | |
| if (letter == line->LastCharacter) | |
| break; | |
| letter = letter->next; | |
| } | |
| } | |
| Add cycle detection and sanity limits. Offset +0x00 is packed UTF-8, not necessarily a null-terminated string: determine the 1–4 byte length from the leading byte. | |
| Do not assume page->cursorPos means mouse position—it is undocumented and probably related to typewriter progression. | |
| 4. Capture line positions | |
| P3R also exposes a lower function currently typed as: | |
| void DrawSingleLineText( | |
| float posX, | |
| float posY, | |
| float posZ, | |
| FSprColor color, | |
| float a5, | |
| nint characterList, | |
| int drawTypeId, | |
| int a8, | |
| long a9, | |
| byte a10); | |
| Its known call-site signature is: | |
| E8 ?? ?? ?? ?? 0F 28 05 ?? ?? ?? ?? | |
| 48 8D 8D ?? ?? ?? ?? F3 44 0F 58 0D ?? ?? ?? ?? | |
| Resolve the E8 rel32 target. In a known speaker-name call, characterList is CurrentSpeaker->CharacterData->Next. | |
| Use thread-local state to capture DrawSingleLineText calls made while inside your DrawMessageText detour. That gives you each line’s actual origin and its corresponding character list. | |
| 5. Get exact glyph rectangles | |
| This is the one Persona-specific detail not publicly documented. | |
| For an exact implementation: | |
| Break on the resolved DrawSingleLineText. | |
| Step until its loop reads character bytes at [letter+0x00]. | |
| Find the corresponding read of [letter+0x14] for color. | |
| Follow the routine that advances X or submits the glyph quad. | |
| Hook that routine and capture: | |
| BmdDataLetter* | |
| x0, y0, x1, y1 | |
| scale/style | |
| surface/window identity | |
| Record final viewport rectangles if possible. Then your mouse and glyphs are already in the same coordinate system, avoiding DPI, ultrawide, and letterboxing problems. | |
| For a quick prototype, P3R’s Japanese glyphs can be treated as fixed-width and indexed from the captured line origin. That will fail eventually on proportional Latin characters, punctuation, inline scaling, or unusual layouts. Capturing the real glyph quad is the production solution. | |
| 6. Hit-test and tokenize | |
| Maintain: | |
| visible Rune index ↔ BmdDataLetter* ↔ glyph rectangle | |
| On mouse movement: | |
| Find the glyph rectangle containing the cursor. | |
| Find the token containing that Rune. | |
| Look up that token/deinflected forms in Jitendex. | |
| Store the corresponding letter nodes as the hovered span. | |
| Use Sudachi/MeCab or Yomitan-style longest-prefix/deinflection logic. Avoid UTF-16 indices; one game letter node corresponds to a Unicode Rune, not necessarily one C# char. | |
| 7. Highlight through P3R | |
| Before calling the original renderer: | |
| var original = node->color; | |
| node->color = new FSprColor(0xFF, 0xEA, 0x35, original.A); | |
| After rendering, restore the original value in finally. | |
| #FFEA35 is one of P3R’s published built-in message colors and visually matches the demo’s yellow. See FeMC’s message color table. | |
| If P3R queues pointers rather than copying the color during the original call, restore on the following frame instead. | |
| 8. Draw the popup | |
| The easiest robust choice for P3R is an in-engine UMG/Canvas overlay. P3R’s public wrappers expose mouse-position and text-drawing functions. A second option is a DXGI Present hook with Dear ImGui, but P3R defaults to DirectX 12, so a DX11-only Persona 5 overlay is not drop-in. | |
| The popup itself can simply be anchored to the mouse and clamped to the viewport; it does not need the selected word’s coordinates. | |
| If you use logical UI coordinates externally, vanilla mapping starts with: | |
| scale = min(viewportWidth / 1920, viewportHeight / 1080) | |
| offsetX = (viewportWidth - 1920 * scale) / 2 | |
| offsetY = (viewportHeight - 1080 * scale) / 2 | |
| But capture final viewport-space glyph geometry when supporting ultrawide or UI-fix mods. | |
| You need multiple P3R adapters | |
| The video demonstrates more than one text surface. At minimum: | |
| UMsgProcWindow_Simple: standard bottom dialogue. | |
| UMsgProcWindow_Mind: centered/internal/environmental text. | |
| UMsgProcWindow_Select_Simple: choices. | |
| Separate adapters later for tutorials, system windows, subtitles, etc. | |
| Each has a different draw function/layout. This is probably what the author means by “individual fine tuning”—identifying each game’s relevant message/render functions, not hand-entering one rectangle. | |
| Why the existing P3R Agent hook is insufficient | |
| The public P3R Agent script hooks decoder instructions and reads text from r9, rdi, and rax. It produces strings but no X/Y or glyph boxes. | |
| That is a standard text hook. The Reddit prototype must hook farther downstream, around layout/rendering, or reproduce the renderer’s layout. | |
| Persona 5/Royal | |
| The architecture is the same but none of the P3R offsets apply. Public P5R reversing documents: | |
| CalcMsgMng::DrawText(MessageScript1*) 0x1417890b0 | |
| CalcMsgMng::DrawMessageBox(MessageScript1*) 0x14178b710 | |
| Those addresses are for one build only. See P5R CalcMsgMng documentation. | |
| For P5R: | |
| Signature-scan CalcMsgMng::DrawText. | |
| Trace into its font/glyph renderer. | |
| Capture the custom Atlus-decoded character, final quad, and color argument. | |
| Override the color for the hovered token. | |
| Build a separate P5R adapter/profile. | |
| So the precise answer is: it knows because it hooks Persona’s text renderer at the point where Persona itself has the active character list and layout position. It does not infer coordinates from the image. The only missing piece needed for a fully exact clone is identifying the final P3R glyph-emission call in your current executable; the Reddit author has not published that hook. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment