From 15956fed6bdf3d05a57b8c318076ee17fff0c2c1 Mon Sep 17 00:00:00 2001 From: j3lee Date: Mon, 12 Oct 2026 13:08:22 +0930 Subject: [PATCH 1/4] q_ketsu: import and decompile the overlay --- CMakeLists.txt | 1 + config/symbols.magic-q_ketsu.us.txt | 10 + config/us.yaml | 14 ++ src/magic/q_ketsu.c | 289 ++++++++++++++++++++++++++++ src/magic/q_ketsu.h | 6 + tools/builder/builder/objdiff.go | 1 + 6 files changed, 321 insertions(+) create mode 100644 config/symbols.magic-q_ketsu.us.txt create mode 100644 src/magic/q_ketsu.c create mode 100644 src/magic/q_ketsu.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 14280eff..92e183a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ set(FF7_MAGIC src/magic/brizad.c src/magic/lv5deth.c src/magic/mabaria.c + src/magic/q_ketsu.c src/magic/sennkoku.c src/magic/thunder.c ) diff --git a/config/symbols.magic-q_ketsu.us.txt b/config/symbols.magic-q_ketsu.us.txt new file mode 100644 index 00000000..82e86150 --- /dev/null +++ b/config/symbols.magic-q_ketsu.us.txt @@ -0,0 +1,10 @@ +QKetsuDoubleBufferFlip = 0x801B0010; +QKetsuHitSparkleSpriteTick = 0x801B007C; +QKetsuRisingSparkleSpriteTick = 0x801B0138; +QKetsuSpiralSparkleSpriteTick = 0x801B0264; +QKetsuTrailSparkleSpriteTick = 0x801B03FC; +QKetsuOrbSpriteTick = 0x801B04B4; +QKetsuModelTick = 0x801B08E0; +QKetsuTargetTick = 0x801B0C5C; +QKetsuTargetSetup = 0x801B0F5C; +MAGIC_QKetsu = 0x801B1028; diff --git a/config/us.yaml b/config/us.yaml index 69eaa150..b0902250 100644 --- a/config/us.yaml +++ b/config/us.yaml @@ -807,3 +807,17 @@ overlays: - [0, c, sennkoku] - [0x930, data, sennkoku_model] - [0x559C, data, sennkoku_buffers] + # Q_KETSU.BIN -- effect overlay + - name: q_ketsu + disk_path: disks/us/MAGIC/Q_KETSU.BIN + sha1: 66cda108823bda268f71222de2e8aedb1f55faa7 + base_path: magic + imports: [main, battle] + vram_start: 0x801B0000 + symbol_addrs_path: + - config/symbols.magic-q_ketsu.us.txt + segments: + - [0, rodata, q_ketsu] + - [0x10, c, q_ketsu] + - [0x10A4, data, q_ketsu_model] + - [0xDAE4, data, q_ketsu_buffers] # REVIEW (split): first aligned word after the last non-zero byte; a cut inside the zero fill is byte-neutral diff --git a/src/magic/q_ketsu.c b/src/magic/q_ketsu.c new file mode 100644 index 00000000..86362bd0 --- /dev/null +++ b/src/magic/q_ketsu.c @@ -0,0 +1,289 @@ +//! PSYQ=3.3 CC1=2.6.3 + +#include "common.h" +#include "magic.h" +#include "q_ketsu.h" +#include "../battle/battle.h" + +// Effect shared by Drain, Dance and Eyesight. + +// Primitive buffer pages, one 0xC000 page per double-buffered frame. +extern u8 D_801BDAE8[2][0xC000]; +extern void* D_801D5AE8; +extern SpriteRenderDesc D_801BDA90; +extern SpriteRenderDesc D_801BDA9C; +extern SpriteRenderDesc D_801BDABC; +extern SpriteAnim* D_801BDAA8[]; +extern s32 D_801BD9CC; +extern s32 D_801BD84C; +extern SpriteRenderDesc D_801BDAB0; +extern s32 D_801BD8E4; +extern u8 D_801B10A4[]; +extern ModelRenderDesc D_801B0000; +extern s16 D_801BDAD4; +extern VECTOR D_801BDAD8; +extern SpriteRenderDesc D_801BDAC8; +extern s32 D_801BD9AC; + +void func_800BBA84(u16 arg0, s32 arg1, s32 arg2); +void QKetsuOrbSpriteTick(void); + +// Only StartFrame/AnimationFrame are common to every magic overlay. +typedef struct { + /* 0x00 */ s16 StartFrame; + /* 0x02 */ s16 AnimationFrame; + /* 0x04 */ SVECTOR Pos; + /* 0x0C */ SVECTOR Pos2; + /* 0x14 */ s16 unk14; + /* 0x16 */ s16 unk16; + /* 0x18 */ s16 unk18; + /* 0x1A */ s16 unk1A; + /* 0x1C */ s16 unk1C; + /* 0x1E */ s16 unk1E; +} QKetsuData; // size:0x20 + +extern QKetsuData g_BattleEffectSlots[]; + +// Same as BioraDoubleBufferFlip. +void QKetsuDoubleBufferFlip(void) { + QKetsuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + D_801D5AE8 = D_801BDAE8[effect->AnimationFrame]; + effect->AnimationFrame ^= 1; + if (g_BattleEffectCount < 2) { + effect->StartFrame = -1; + } +} + +void QKetsuHitSparkleSpriteTick(void) { + QKetsuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + BattleSetBillboardMatrix(&effect->Pos, 0x2000, 0); + D_801BDA90.frameIndex = effect->AnimationFrame; + D_801D5AE8 = func_800D4D90(&D_801BDA90, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->AnimationFrame++; + if (effect->AnimationFrame >= D_801BD9CC) { + effect->StartFrame = -1; + } + } +} + +void QKetsuRisingSparkleSpriteTick(void) { + QKetsuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + BattleSetBillboardMatrix(&effect->Pos, 0x2000, 0); + D_801BDA9C.frameIndex = effect->AnimationFrame % D_801BDAA8[effect->unk14]->frameCount; + D_801BDA9C.frames = D_801BDAA8[effect->unk14]; + D_801D5AE8 = func_800D4D90(&D_801BDA9C, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->Pos.vy += 0x64; + effect->AnimationFrame++; + if (effect->AnimationFrame == 10) { + effect->StartFrame = -1; + } + } +} + +void QKetsuSpiralSparkleSpriteTick(void) { + QKetsuData* effect; + SVECTOR vec; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + vec.vx = effect->Pos.vx + (rsin(effect->unk14) * effect->unk18 >> 12); + vec.vz = effect->Pos.vz + (rcos(effect->unk14) * effect->unk18 >> 12); + vec.vy = effect->Pos.vy + effect->unk1A; + BattleSetBillboardMatrix(&vec, 0x2000, 0); + D_801BDAB0.frameIndex = effect->AnimationFrame % D_801BD8E4; + D_801D5AE8 = func_800D4D90(&D_801BDAB0, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->unk1A -= 0x32; + effect->unk14 += 0x80; + effect->unk18 -= effect->unk18 / 10; + effect->AnimationFrame++; + if (effect->AnimationFrame == 20) { + effect->StartFrame = -1; + } + } +} + +void QKetsuTrailSparkleSpriteTick(void) { + QKetsuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + BattleSetBillboardMatrix(&effect->Pos, 0x2000, 0); + D_801BDABC.frameIndex = effect->AnimationFrame; + D_801D5AE8 = func_800D4D90(&D_801BDABC, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->AnimationFrame++; + if (effect->AnimationFrame == D_801BD84C) { + effect->StartFrame = -1; + } + } +} + +void QKetsuOrbSpriteTick(void) { + QKetsuData* effect; + QKetsuData* child; + SVECTOR vec; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + vec.vx = effect->Pos.vx + (rsin(effect->unk14) * effect->unk18 >> 12); + vec.vy = effect->Pos.vy + (rcos(effect->unk14) * effect->unk18 >> 12); + vec.vz = effect->Pos.vz; + BattleSetBillboardMatrix(&vec, (s16)(effect->unk16 * 5 / effect->unk1C), 0); + D_801BDAC8.frameIndex = effect->AnimationFrame % D_801BD9AC; + D_801D5AE8 = func_800D4D90(&D_801BDAC8, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + if (effect->AnimationFrame < 0x1E) { + effect->unk16 += 0xC0; + } + if ((u16)(effect->AnimationFrame - 0x1E) < 10) { + effect->Pos.vx += effect->Pos2.vx; + effect->Pos.vy += effect->Pos2.vy; + effect->Pos.vz += effect->Pos2.vz; + effect->unk18 = (u32)(rsin(effect->unk1A += 0xCC) * 25) >> 8; + effect->unk14 += 0x200; + child = &g_BattleEffectSlots[BattleEffectRegister(QKetsuTrailSparkleSpriteTick)]; + child->Pos.vx = effect->Pos.vx + rand() % 800 - 400; + child->Pos.vy = effect->Pos.vy + rand() % 800 - 400; + child->Pos.vz = effect->Pos.vz + rand() % 800 - 400; + } + if (effect->AnimationFrame >= 0x29) { + effect->unk16 -= 0x100; + child = &g_BattleEffectSlots[BattleEffectRegister(QKetsuRisingSparkleSpriteTick)]; + child->Pos.vx = effect->Pos.vx + rand() % 600 - 300; + child->Pos.vy = effect->Pos.vy + rand() % 600 - 300; + child->Pos.vz = effect->Pos.vz + rand() % 600 - 300; + child->unk14 = rand() & 1; + } + effect->AnimationFrame++; + if (effect->AnimationFrame == 0x3C) { + effect->StartFrame = -1; + } + } +} + +void QKetsuModelTick(void) { + QKetsuData* effect; + QKetsuData* child; + MATRIX mat; + MATRIX view; + ModelRenderDesc desc = D_801B0000; + SVECTOR rot; + s16 i; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + rot.vz = 0; + rot.vx = 0; + rot.vy = effect->AnimationFrame << 7; + RotMatrixYXZ(&rot, &mat); + ScaleMatrix(&mat, &D_801BDAD8); + mat.t[0] = effect->Pos.vx; + mat.t[1] = effect->Pos.vy; + mat.t[2] = effect->Pos.vz; + CompMatrix(&g_BattleWorldView, &mat, &view); + SetRotMatrix(&view); + SetTransMatrix(&view); + desc.color = D_801BDAD4; + desc.clut = effect->AnimationFrame % 5 * 0xC0; + D_801D5AE8 = func_800D29D4(&desc, (u_long**)&g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + if (effect->AnimationFrame < 0x14) { + effect->Pos.vx += effect->Pos2.vx; + effect->Pos.vy += effect->Pos2.vy; + effect->Pos.vz += effect->Pos2.vz; + D_801BDAD4 += 6; + } + if ((u16)(effect->AnimationFrame - 0x14) < 0x1E) { + for (i = 0; i < 1; i++) { + child = &g_BattleEffectSlots[BattleEffectRegister(QKetsuSpiralSparkleSpriteTick)]; + child->Pos.vx = effect->Pos.vx; + child->Pos.vy = effect->Pos.vy; + child->Pos.vz = effect->Pos.vz; + child->unk14 = rand() & 0xFFF; + child->unk18 = 1000; + child->unk1A = 1000; + } + } + if ((u16)(effect->AnimationFrame - 0x26) < 6) { + if (!(effect->AnimationFrame % 2)) { + child = &g_BattleEffectSlots[BattleEffectRegister(QKetsuOrbSpriteTick)]; + child->Pos.vx = effect->Pos.vx; + child->Pos.vy = effect->Pos.vy - 0x1F4; + child->Pos.vz = effect->Pos.vz; + child->Pos2.vx = -(u16)effect->Pos2.vx * 2; + child->Pos2.vy = -(u16)effect->Pos2.vy * 2; + child->Pos2.vz = -(u16)effect->Pos2.vz * 2; + child->unk16 = 0x100; + child->unk1A = 0; + child->unk18 = 0; + child->unk1C = (effect->AnimationFrame - 0x26) / 2 + 5; + } + } + if (effect->AnimationFrame >= 0x33) { + if (!(D_801BDAD4 -= 6)) { + effect->StartFrame = -1; + } + } + effect->AnimationFrame++; + } +} + +void QKetsuTargetTick(void) { + QKetsuData* effect; + QKetsuData* child; + s32 y; + s16 i; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + if (!D_80062D98) { + if (!effect->AnimationFrame) { + child = &g_BattleEffectSlots[BattleEffectRegister(QKetsuModelTick)]; + child->Pos.vx = effect->Pos.vx; + child->Pos.vy = effect->Pos.vy - 0x190; + child->Pos.vz = effect->Pos.vz; + child->Pos2.vx = (effect->Pos2.vx - child->Pos.vx) / 20; + y = child->Pos.vy + 0x320; + child->Pos2.vy = (effect->Pos2.vy - y) / 20; + child->Pos2.vz = (effect->Pos2.vz - child->Pos.vz) / 20; + } + if (effect->AnimationFrame == 0x5A) { + func_800D5774(effect->unk1C); + func_800BBA84(7, effect->unk1E, 0x40); + } + if (effect->AnimationFrame >= 0x57 && effect->AnimationFrame < 0x66) { + for (i = 0; i < 4; i++) { + child = &g_BattleEffectSlots[BattleEffectRegister(QKetsuHitSparkleSpriteTick)]; + BattleGetPartPosition(effect->unk1E, rand() % g_BattleModels[effect->unk1E].numBones, &child->Pos); + child->Pos.vx += rand() % 600 - 300; + child->Pos.vy += rand() % 600 - 300; + child->Pos.vz += rand() % 600 - 300; + } + } + effect->AnimationFrame++; + if (effect->AnimationFrame == 0x6E) { + effect->StartFrame = -1; + } + } +} + +void QKetsuTargetSetup(s32 target, s32 target2) { + QKetsuData* effect; + + effect = &g_BattleEffectSlots[BattleEffectRegister(QKetsuTargetTick)]; + BattleGetPartPosition(target2, g_BattleModels[target2].boneIndices[0], &effect->Pos); + BattleGetPartPosition(target, g_BattleModels[target].boneIndices[0], &effect->Pos2); + effect->unk1C = target; + effect->unk1E = target2; +} + +void MAGIC_QKetsu(s32 targetMask, s32 callbackArg) { + BattleEffectRegister(QKetsuDoubleBufferFlip); + BattleSetLoadTimToVram(D_801B10A4, 0, 0, 0); + MagicAnimationRegister(targetMask, callbackArg, 4, QKetsuTargetSetup); + func_800BBA84(0x7E, callbackArg, 0x40); +} diff --git a/src/magic/q_ketsu.h b/src/magic/q_ketsu.h new file mode 100644 index 00000000..b1f14f02 --- /dev/null +++ b/src/magic/q_ketsu.h @@ -0,0 +1,6 @@ +#ifndef Q_KETSU_H +#define Q_KETSU_H + +#include "magic.h" + +#endif diff --git a/tools/builder/builder/objdiff.go b/tools/builder/builder/objdiff.go index 23967455..6d460f4f 100644 --- a/tools/builder/builder/objdiff.go +++ b/tools/builder/builder/objdiff.go @@ -113,6 +113,7 @@ var metaOverlays = map[string]metaOverlay{ "lv5deth": {"lv5deth", "magic", "LV.5 Death"}, "choco0": {"choco0", "magic", "choco0"}, "sennkoku": {"sennkoku", "magic", "Death Sentence / Roulette"}, + "q_ketsu": {"q_ketsu", "magic", "Q_KETSU"}, } func makeObjdiffConfig(b BuildConfig) objdiffConfig { From 01520e0dda740837ebd53af9aa345c91269826ff Mon Sep 17 00:00:00 2001 From: j3lee Date: Mon, 12 Oct 2026 13:08:22 +0930 Subject: [PATCH 2/4] q_shu: import and decompile the overlay --- CMakeLists.txt | 1 + config/symbols.magic-q_shu.us.txt | 10 ++ config/us.yaml | 14 ++ src/magic/q_shu.c | 289 ++++++++++++++++++++++++++++++ src/magic/q_shu.h | 6 + tools/builder/builder/objdiff.go | 1 + 6 files changed, 321 insertions(+) create mode 100644 config/symbols.magic-q_shu.us.txt create mode 100644 src/magic/q_shu.c create mode 100644 src/magic/q_shu.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 92e183a2..d7172a18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ set(FF7_MAGIC src/magic/lv5deth.c src/magic/mabaria.c src/magic/q_ketsu.c + src/magic/q_shu.c src/magic/sennkoku.c src/magic/thunder.c ) diff --git a/config/symbols.magic-q_shu.us.txt b/config/symbols.magic-q_shu.us.txt new file mode 100644 index 00000000..f7abd24e --- /dev/null +++ b/config/symbols.magic-q_shu.us.txt @@ -0,0 +1,10 @@ +QShuDoubleBufferFlip = 0x801B0010; +QShuHitSparkleSpriteTick = 0x801B007C; +QShuRisingSparkleSpriteTick = 0x801B0138; +QShuSpiralSparkleSpriteTick = 0x801B0264; +QShuTrailSparkleSpriteTick = 0x801B03FC; +QShuOrbSpriteTick = 0x801B04B4; +QShuModelTick = 0x801B08E0; +QShuTargetTick = 0x801B0C5C; +QShuTargetSetup = 0x801B0F5C; +MAGIC_QShu = 0x801B1028; diff --git a/config/us.yaml b/config/us.yaml index b0902250..04d0f36c 100644 --- a/config/us.yaml +++ b/config/us.yaml @@ -821,3 +821,17 @@ overlays: - [0x10, c, q_ketsu] - [0x10A4, data, q_ketsu_model] - [0xDAE4, data, q_ketsu_buffers] # REVIEW (split): first aligned word after the last non-zero byte; a cut inside the zero fill is byte-neutral + # Q_SHU.BIN -- effect overlay + - name: q_shu + disk_path: disks/us/MAGIC/Q_SHU.BIN + sha1: e180f519db423f6eb419203f59abe619f88b5c11 + base_path: magic + imports: [main, battle] + vram_start: 0x801B0000 + symbol_addrs_path: + - config/symbols.magic-q_shu.us.txt + segments: + - [0, rodata, q_shu] + - [0x10, c, q_shu] + - [0x10A4, data, q_shu_model] + - [0xDAE4, data, q_shu_buffers] # REVIEW (split): first aligned word after the last non-zero byte; a cut inside the zero fill is byte-neutral diff --git a/src/magic/q_shu.c b/src/magic/q_shu.c new file mode 100644 index 00000000..62f77aa3 --- /dev/null +++ b/src/magic/q_shu.c @@ -0,0 +1,289 @@ +//! PSYQ=3.3 CC1=2.6.3 + +#include "common.h" +#include "magic.h" +#include "q_shu.h" +#include "../battle/battle.h" + +// Effect shared by Aspil, Energy Siphon, Magic Cutter, Dance and Eyesight. + +// Primitive buffer pages, one 0xC000 page per double-buffered frame. +extern u8 D_801BDAE8[2][0xC000]; +extern void* D_801D5AE8; +extern SpriteRenderDesc D_801BDA90; +extern SpriteRenderDesc D_801BDA9C; +extern SpriteRenderDesc D_801BDABC; +extern SpriteAnim* D_801BDAA8[]; +extern s32 D_801BD9CC; +extern s32 D_801BD84C; +extern SpriteRenderDesc D_801BDAB0; +extern s32 D_801BD8E4; +extern u8 D_801B10A4[]; +extern ModelRenderDesc D_801B0000; +extern s16 D_801BDAD4; +extern VECTOR D_801BDAD8; +extern SpriteRenderDesc D_801BDAC8; +extern s32 D_801BD9AC; + +void func_800BBA84(u16 arg0, s32 arg1, s32 arg2); +void QShuOrbSpriteTick(void); + +// Only StartFrame/AnimationFrame are common to every magic overlay. +typedef struct { + /* 0x00 */ s16 StartFrame; + /* 0x02 */ s16 AnimationFrame; + /* 0x04 */ SVECTOR Pos; + /* 0x0C */ SVECTOR Pos2; + /* 0x14 */ s16 unk14; + /* 0x16 */ s16 unk16; + /* 0x18 */ s16 unk18; + /* 0x1A */ s16 unk1A; + /* 0x1C */ s16 unk1C; + /* 0x1E */ s16 unk1E; +} QShuData; // size:0x20 + +extern QShuData g_BattleEffectSlots[]; + +// Same as BioraDoubleBufferFlip. +void QShuDoubleBufferFlip(void) { + QShuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + D_801D5AE8 = D_801BDAE8[effect->AnimationFrame]; + effect->AnimationFrame ^= 1; + if (g_BattleEffectCount < 2) { + effect->StartFrame = -1; + } +} + +void QShuHitSparkleSpriteTick(void) { + QShuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + BattleSetBillboardMatrix(&effect->Pos, 0x2000, 0); + D_801BDA90.frameIndex = effect->AnimationFrame; + D_801D5AE8 = func_800D4D90(&D_801BDA90, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->AnimationFrame++; + if (effect->AnimationFrame >= D_801BD9CC) { + effect->StartFrame = -1; + } + } +} + +void QShuRisingSparkleSpriteTick(void) { + QShuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + BattleSetBillboardMatrix(&effect->Pos, 0x2000, 0); + D_801BDA9C.frameIndex = effect->AnimationFrame % D_801BDAA8[effect->unk14]->frameCount; + D_801BDA9C.frames = D_801BDAA8[effect->unk14]; + D_801D5AE8 = func_800D4D90(&D_801BDA9C, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->Pos.vy += 0x64; + effect->AnimationFrame++; + if (effect->AnimationFrame == 10) { + effect->StartFrame = -1; + } + } +} + +void QShuSpiralSparkleSpriteTick(void) { + QShuData* effect; + SVECTOR vec; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + vec.vx = effect->Pos.vx + (rsin(effect->unk14) * effect->unk18 >> 12); + vec.vz = effect->Pos.vz + (rcos(effect->unk14) * effect->unk18 >> 12); + vec.vy = effect->Pos.vy + effect->unk1A; + BattleSetBillboardMatrix(&vec, 0x2000, 0); + D_801BDAB0.frameIndex = effect->AnimationFrame % D_801BD8E4; + D_801D5AE8 = func_800D4D90(&D_801BDAB0, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->unk1A -= 0x32; + effect->unk14 += 0x80; + effect->unk18 -= effect->unk18 / 10; + effect->AnimationFrame++; + if (effect->AnimationFrame == 20) { + effect->StartFrame = -1; + } + } +} + +void QShuTrailSparkleSpriteTick(void) { + QShuData* effect; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + BattleSetBillboardMatrix(&effect->Pos, 0x2000, 0); + D_801BDABC.frameIndex = effect->AnimationFrame; + D_801D5AE8 = func_800D4D90(&D_801BDABC, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + effect->AnimationFrame++; + if (effect->AnimationFrame == D_801BD84C) { + effect->StartFrame = -1; + } + } +} + +void QShuOrbSpriteTick(void) { + QShuData* effect; + QShuData* child; + SVECTOR vec; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + vec.vx = effect->Pos.vx + (rsin(effect->unk14) * effect->unk18 >> 12); + vec.vy = effect->Pos.vy + (rcos(effect->unk14) * effect->unk18 >> 12); + vec.vz = effect->Pos.vz; + BattleSetBillboardMatrix(&vec, (s16)(effect->unk16 * 5 / effect->unk1C), 0); + D_801BDAC8.frameIndex = effect->AnimationFrame % D_801BD9AC; + D_801D5AE8 = func_800D4D90(&D_801BDAC8, g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + if (effect->AnimationFrame < 0x1E) { + effect->unk16 += 0xC0; + } + if ((u16)(effect->AnimationFrame - 0x1E) < 10) { + effect->Pos.vx += effect->Pos2.vx; + effect->Pos.vy += effect->Pos2.vy; + effect->Pos.vz += effect->Pos2.vz; + effect->unk18 = (u32)(rsin(effect->unk1A += 0xCC) * 25) >> 8; + effect->unk14 += 0x200; + child = &g_BattleEffectSlots[BattleEffectRegister(QShuTrailSparkleSpriteTick)]; + child->Pos.vx = effect->Pos.vx + rand() % 800 - 400; + child->Pos.vy = effect->Pos.vy + rand() % 800 - 400; + child->Pos.vz = effect->Pos.vz + rand() % 800 - 400; + } + if (effect->AnimationFrame >= 0x29) { + effect->unk16 -= 0x100; + child = &g_BattleEffectSlots[BattleEffectRegister(QShuRisingSparkleSpriteTick)]; + child->Pos.vx = effect->Pos.vx + rand() % 600 - 300; + child->Pos.vy = effect->Pos.vy + rand() % 600 - 300; + child->Pos.vz = effect->Pos.vz + rand() % 600 - 300; + child->unk14 = rand() & 1; + } + effect->AnimationFrame++; + if (effect->AnimationFrame == 0x3C) { + effect->StartFrame = -1; + } + } +} + +void QShuModelTick(void) { + QShuData* effect; + QShuData* child; + MATRIX mat; + MATRIX view; + ModelRenderDesc desc = D_801B0000; + SVECTOR rot; + s16 i; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + rot.vz = 0; + rot.vx = 0; + rot.vy = effect->AnimationFrame << 7; + RotMatrixYXZ(&rot, &mat); + ScaleMatrix(&mat, &D_801BDAD8); + mat.t[0] = effect->Pos.vx; + mat.t[1] = effect->Pos.vy; + mat.t[2] = effect->Pos.vz; + CompMatrix(&g_BattleWorldView, &mat, &view); + SetRotMatrix(&view); + SetTransMatrix(&view); + desc.color = D_801BDAD4; + desc.clut = effect->AnimationFrame % 5 * 0xC0; + D_801D5AE8 = func_800D29D4(&desc, (u_long**)&g_cDb->unk70, 12, D_801D5AE8); + if (!D_80062D98) { + if (effect->AnimationFrame < 0x14) { + effect->Pos.vx += effect->Pos2.vx; + effect->Pos.vy += effect->Pos2.vy; + effect->Pos.vz += effect->Pos2.vz; + D_801BDAD4 += 6; + } + if ((u16)(effect->AnimationFrame - 0x14) < 0x1E) { + for (i = 0; i < 1; i++) { + child = &g_BattleEffectSlots[BattleEffectRegister(QShuSpiralSparkleSpriteTick)]; + child->Pos.vx = effect->Pos.vx; + child->Pos.vy = effect->Pos.vy; + child->Pos.vz = effect->Pos.vz; + child->unk14 = rand() & 0xFFF; + child->unk18 = 1000; + child->unk1A = 1000; + } + } + if ((u16)(effect->AnimationFrame - 0x26) < 6) { + if (!(effect->AnimationFrame % 2)) { + child = &g_BattleEffectSlots[BattleEffectRegister(QShuOrbSpriteTick)]; + child->Pos.vx = effect->Pos.vx; + child->Pos.vy = effect->Pos.vy - 0x1F4; + child->Pos.vz = effect->Pos.vz; + child->Pos2.vx = -(u16)effect->Pos2.vx * 2; + child->Pos2.vy = -(u16)effect->Pos2.vy * 2; + child->Pos2.vz = -(u16)effect->Pos2.vz * 2; + child->unk16 = 0x100; + child->unk1A = 0; + child->unk18 = 0; + child->unk1C = (effect->AnimationFrame - 0x26) / 2 + 5; + } + } + if (effect->AnimationFrame >= 0x33) { + if (!(D_801BDAD4 -= 6)) { + effect->StartFrame = -1; + } + } + effect->AnimationFrame++; + } +} + +void QShuTargetTick(void) { + QShuData* effect; + QShuData* child; + s32 y; + s16 i; + + effect = &g_BattleEffectSlots[g_BattleEffectCursor]; + if (!D_80062D98) { + if (!effect->AnimationFrame) { + child = &g_BattleEffectSlots[BattleEffectRegister(QShuModelTick)]; + child->Pos.vx = effect->Pos.vx; + child->Pos.vy = effect->Pos.vy - 0x190; + child->Pos.vz = effect->Pos.vz; + child->Pos2.vx = (effect->Pos2.vx - child->Pos.vx) / 20; + y = child->Pos.vy + 0x320; + child->Pos2.vy = (effect->Pos2.vy - y) / 20; + child->Pos2.vz = (effect->Pos2.vz - child->Pos.vz) / 20; + } + if (effect->AnimationFrame == 0x5A) { + func_800D5774(effect->unk1C); + func_800BBA84(0x1F, effect->unk1E, 0x40); + } + if (effect->AnimationFrame >= 0x57 && effect->AnimationFrame < 0x66) { + for (i = 0; i < 4; i++) { + child = &g_BattleEffectSlots[BattleEffectRegister(QShuHitSparkleSpriteTick)]; + BattleGetPartPosition(effect->unk1E, rand() % g_BattleModels[effect->unk1E].numBones, &child->Pos); + child->Pos.vx += rand() % 600 - 300; + child->Pos.vy += rand() % 600 - 300; + child->Pos.vz += rand() % 600 - 300; + } + } + effect->AnimationFrame++; + if (effect->AnimationFrame == 0x6E) { + effect->StartFrame = -1; + } + } +} + +void QShuTargetSetup(s32 target, s32 target2) { + QShuData* effect; + + effect = &g_BattleEffectSlots[BattleEffectRegister(QShuTargetTick)]; + BattleGetPartPosition(target2, g_BattleModels[target2].boneIndices[0], &effect->Pos); + BattleGetPartPosition(target, g_BattleModels[target].boneIndices[0], &effect->Pos2); + effect->unk1C = target; + effect->unk1E = target2; +} + +void MAGIC_QShu(s32 targetMask, s32 callbackArg) { + BattleEffectRegister(QShuDoubleBufferFlip); + BattleSetLoadTimToVram(D_801B10A4, 0, 0, 0); + MagicAnimationRegister(targetMask, callbackArg, 4, QShuTargetSetup); + func_800BBA84(0x7E, callbackArg, 0x40); +} diff --git a/src/magic/q_shu.h b/src/magic/q_shu.h new file mode 100644 index 00000000..dfadd9ee --- /dev/null +++ b/src/magic/q_shu.h @@ -0,0 +1,6 @@ +#ifndef Q_SHU_H +#define Q_SHU_H + +#include "magic.h" + +#endif diff --git a/tools/builder/builder/objdiff.go b/tools/builder/builder/objdiff.go index 6d460f4f..f02b0b06 100644 --- a/tools/builder/builder/objdiff.go +++ b/tools/builder/builder/objdiff.go @@ -114,6 +114,7 @@ var metaOverlays = map[string]metaOverlay{ "choco0": {"choco0", "magic", "choco0"}, "sennkoku": {"sennkoku", "magic", "Death Sentence / Roulette"}, "q_ketsu": {"q_ketsu", "magic", "Q_KETSU"}, + "q_shu": {"q_shu", "magic", "Q_SHU"}, } func makeObjdiffConfig(b BuildConfig) objdiffConfig { From d849e831bc7063ed3edc456d9b980e1273127e24 Mon Sep 17 00:00:00 2001 From: j3lee Date: Mon, 12 Oct 2026 13:09:23 +0930 Subject: [PATCH 3/4] magic: name the effects in the file headers --- src/magic/q_ketsu.c | 2 +- src/magic/q_shu.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/magic/q_ketsu.c b/src/magic/q_ketsu.c index 86362bd0..41753f7a 100644 --- a/src/magic/q_ketsu.c +++ b/src/magic/q_ketsu.c @@ -5,7 +5,7 @@ #include "q_ketsu.h" #include "../battle/battle.h" -// Effect shared by Drain, Dance and Eyesight. +// Effect shared by Drain (ドレイン), Dance (おどり) and Eyesight (眼力). // Primitive buffer pages, one 0xC000 page per double-buffered frame. extern u8 D_801BDAE8[2][0xC000]; diff --git a/src/magic/q_shu.c b/src/magic/q_shu.c index 62f77aa3..81b3474b 100644 --- a/src/magic/q_shu.c +++ b/src/magic/q_shu.c @@ -5,7 +5,8 @@ #include "q_shu.h" #include "../battle/battle.h" -// Effect shared by Aspil, Energy Siphon, Magic Cutter, Dance and Eyesight. +// Effect shared by Aspil (アスピル), Energy Siphon (エネルギー吸収), Magic Cutter (魔吸軟骨剣), +// Dance (おどり) and Eyesight (眼力). // Primitive buffer pages, one 0xC000 page per double-buffered frame. extern u8 D_801BDAE8[2][0xC000]; From 0eafd8031afdebcfcd931109df0f1023fae8b492 Mon Sep 17 00:00:00 2001 From: j3lee Date: Mon, 12 Oct 2026 13:12:47 +0930 Subject: [PATCH 4/4] magic: point the "same as" comments at upstream functions Cites functions that exist on origin/main (or in this PR); comments that named draft-only functions are repointed or dropped. --- src/magic/q_ketsu.c | 2 +- src/magic/q_shu.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/magic/q_ketsu.c b/src/magic/q_ketsu.c index 41753f7a..a8df96df 100644 --- a/src/magic/q_ketsu.c +++ b/src/magic/q_ketsu.c @@ -44,7 +44,7 @@ typedef struct { extern QKetsuData g_BattleEffectSlots[]; -// Same as BioraDoubleBufferFlip. +// Same as BarrierDoubleBufferFlip. void QKetsuDoubleBufferFlip(void) { QKetsuData* effect; diff --git a/src/magic/q_shu.c b/src/magic/q_shu.c index 81b3474b..86f5dda2 100644 --- a/src/magic/q_shu.c +++ b/src/magic/q_shu.c @@ -45,7 +45,7 @@ typedef struct { extern QShuData g_BattleEffectSlots[]; -// Same as BioraDoubleBufferFlip. +// Same as BarrierDoubleBufferFlip. void QShuDoubleBufferFlip(void) { QShuData* effect;