Skip to content

Commit 84ca17d

Browse files
committed
Reconstruct retail generic effect descriptors
1 parent 002c89c commit 84ca17d

12 files changed

Lines changed: 281 additions & 16 deletions

File tree

documentation/exe-analysis.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,6 +1819,16 @@ judgement, the complete combat packet, the authored delay, and the remaining
18191819
constructor values. The list updater at `0x0042fd60` keeps that node until the
18201820
handler selected by `0x00429ec0` returns zero.
18211821

1822+
The low raw effect numbers do not enter the specialized controller families.
1823+
Dispatcher `0x00429ec0` sends values `0..7` to generic initializer
1824+
`0x0042a300` with OPTION resources `0,1,2,3,0,0,4,0`. This includes all
1825+
shipped direct-special values `0,1,4,5,6,7`. The normal direct forms use
1826+
30-unit bounds, whereas type six uses `[-160,-160,159,159]` and contact
1827+
visual 21023. They project from the live owner by constructor field 21, carry
1828+
the packet, collide with scenery and the first target, and use contact sample
1829+
20. This makes the raw family a generic runtime actor, not a truncated form
1830+
of effect `10001`.
1831+
18221832
All twelve nonnegative effect types found in the shipped enemy records have a
18231833
specialized branch. The dispatch covers types 1, 2, 3, 4, 5, 10, 11, 12, 13,
18241834
14, 16, and 21. This is separate from the much simpler 21000-series receiver

reverse/shadowflare-exe/functions.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ address,name,reconstructed_symbol,status,evidence,notes
9595
0x00429ce0,UpdateScenarioEntities,osf::WorldScene::update,partial,static,Every active-map dynamic entity receives its virtual update without a camera or render-clip test; objects PEOPLE enemies and settled items update offscreen in class and local-ID order while complete enemy AI remains
9696
0x00429dd0,CreateRuntimeEffectActor,osf::RuntimeEffectActor::initialize,partial,static,Creates a category-50000000 actor from a 126-word descriptor after the effect controller has resolved its resource position bounds chart target window packet environment collision policy and lifetime fields; all twelve specialized enemy-effect controller families now have portable actor requests and shipped live coverage
9797
0x00429ec0,DispatchWorldEffect,osf::CombatEffectActor::initialize / osf::EnemyEffectController::initialize,partial,static,Dispatches 10001 10002 10003 10004 10005 10010 10011 10012 10013 10014 10016 and 10021 to twelve specialized controllers and maps simple effects 21000 through 21014 plus 21019 and Heal visual 21020 to their exact Character OPTION resources; every specialized enemy controller has portable outputs and live shipped coverage
98-
0x0042a300,InitializeGenericEffectActor,osf::buildGenericEffectActor / osf::RuntimeEffectSystem::queueActor,implemented,static,Maps generic effects zero one three four five seven and Sonic Blade 10015 to OPTION resources and creates category-50000000 actors with effect-specific bounds origin projection speed height lifetime straight or homing motion target and environment collision contact sample twenty optional remembered targets and copied packets
98+
0x0042a300,InitializeGenericEffectActor,osf::buildGenericEffectActor / sf_generic_effect_actor_build / osf::RuntimeEffectSystem::queueActor,partial,static,Dispatches raw effects zero through seven to OPTION resources 0 1 2 3 0 0 4 0; the C99 direct-special descriptors cover shipped raw kinds zero one four five six and seven with exact integer projection bounds direction packet scenery and first-target expiry contact sample twenty and type-six contact visual 21023 while type two and the live fixed actor owner remain
9999
0x0042a860,UpdateEffect10004Controller,osf::EnemyEffectController::update,implemented,static,Update three emits warning resource 10000002 at the re-resolved source; delay ten re-resolves it again and emits two resource-10000000 chart layers samples 29 and 23 an invisible one-update area packet with contact sample 20 and the nearby eight-update magnitude-six vertical camera shake
100100
0x0042ae40,UpdateEffect10001Controller,osf::EnemyEffectController::update,implemented,static,Update zero emits resource 10000012 with one chart-zero lifetime at the resolved owner and constructor delay 12 re-resolves that owner then emits resource 10000010 exactly 180 units along the stored angle with fifty-unit bounds forward speed contact expiry copied packet and positional sample 19; zero negative and missing-owner timing have focused coverage
101101
0x0042b1c0,UpdateEffect10002Controller,osf::EnemyEffectController::update,implemented,static,Update zero emits resource 11000027 with one chart-zero lifetime at the resolved owner and constructor delay 12 re-resolves that owner then emits resource 10000040 exactly 180 units along the stored angle with fifty-unit bounds forward speed contact expiry copied packet and positional sample 94; zero positive and missing-owner timing have focused coverage

reverse/shadowflare-exe/status.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,25 @@ remaining authored constructor values. `0x0042fd60` updates this controller
2222
list and removes a node only when its selected handler returns zero.
2323

2424
`0x00429ec0` is the controller dispatch, not a visual-resource lookup table.
25-
The twelve effect numbers present in shipped enemy profiles dispatch as
26-
follows:
25+
Before the `10001+` controller switch, raw effects `0..7` all dispatch to the
26+
generic actor initializer at `0x0042a300`. Its OPTION-resource arguments are
27+
`0,1,2,3,0,0,4,0`. Effects 0, 4, 5, and 7 therefore deliberately share
28+
resource zero; effect 1 uses resource one; and the shipped direct-special
29+
effect 6 uses resource four. The ordinary direct-special set is
30+
`0/1/4/5/6/7`, so none of those values belongs to the specialized controller
31+
table below.
32+
33+
The direct generic forms project a live source by constructor argument 21,
34+
move at argument six, draw at height seven, use chart zero, collide with the
35+
environment and the first eligible target, retain the packet, and request
36+
sample 20 on contact. Their normal judgement is `[-30,-30,30,30]`; effect 6
37+
instead uses `[-160,-160,159,159]` and contact visual 21023. Effect 1 always
38+
draws direction eight, while the others derive direction from the stored
39+
angle when packet kind is eight. The integer-only C99 descriptor preserves
40+
the unreduced source-to-target vector and projects it without floating point.
41+
42+
`0x00429ec0` then dispatches the twelve effect numbers present in shipped
43+
enemy profiles as follows:
2744

2845
| Effect | Controller |
2946
|---:|---:|

roadmap.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,15 @@ the main screen arena untouched, and can be rewound without touching the
204204
fixed framebuffer. This is a generic resource/chart boundary, not a list of
205205
special maps or Goblin IDs.
206206

207-
The immediate target is the retail consumer for raw direct-special effect kinds
208-
`0/1/4/5/6/7`, followed by enemy presentation actions five through seven.
209-
Those raw kinds must not be guessed to be the already reconstructed `10001+`
210-
projectile controllers. Retail tracing should identify their lifetime and
211-
resource rules before the bounded request queue is consumed. Hit/death CAF
212-
presentation can then consume the reaction state already published by the
213-
receivers.
207+
The immediate target is the fixed generic-effect actor owner and its bounded
208+
OPTION resource bank, followed by enemy presentation actions five through
209+
seven. Retail dispatch is now proven: raw direct-special kinds `0/1/4/5/6/7`
210+
map to resources `0/1/0/0/4/0`, not the `10001+` projectile controllers. The
211+
integer-only descriptor boundary preserves projection, bounds, direction,
212+
packet and collision policy. The next slice should load only the required
213+
resource, advance actors without allocation, and keep their state outside the
214+
renderer. Hit/death CAF presentation can then consume the reaction state
215+
already published by the receivers.
214216

215217
## Where we are now
216218

shadowflare/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ add_library(
101101
game/enemy_direct_impact.c
102102
game/game.c
103103
game/gameplay_service.c
104+
game/generic_effect_actor.c
104105
game/ground_item.c
105106
game/inventory.c
106107
game/item_condition.c

shadowflare/README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,10 +444,19 @@ It retains the exact entry target vector using integer coordinates, consumes
444444
both retail visual draws, applies the raw effect-kind packet switch, and writes
445445
the complete 22-argument request into a fixed eight-entry frame queue. The
446446
queue reports saturation and clears by resetting two small fields; it does not
447-
zero or allocate packet storage in the update hot path. The raw retail kinds
448-
`0/1/4/5/6/7` deliberately remain requests until their executable consumer is
449-
reconstructed, rather than being misidentified as the unrelated `10001+`
450-
projectile family.
447+
zero or allocate packet storage in the update hot path. At this enqueue
448+
boundary the raw retail kinds `0/1/4/5/6/7` deliberately remain requests,
449+
rather than being misidentified as the unrelated `10001+` projectile family.
450+
451+
The retail consumer for those raw requests is now identified and represented
452+
as a separate integer-only actor descriptor. Direct kinds `0/1/4/5/6/7` map
453+
to OPTION resources `0/1/0/0/4/0`; the builder retains source ownership,
454+
packet, speed, height, exact projected origin, direction, scenery and
455+
first-target expiry, and contact sample 20. Kind 6 also keeps its larger
456+
`[-160,-160,159,159]` judgement and contact visual 21023. Asset lifetime,
457+
movement, collision dispatch, and drawing remain the next fixed actor-owner
458+
slice rather than leaking into this packet translator.
459+
451460
Those rules live in small `game/` files and never enter movement or rendering.
452461

453462
Only the needed cells from combat tables 7, 11, 24, 25, and 26 are retained in
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (C) 2026 Michael Binder and contributors
3+
*
4+
* This file is part of OpenShadowFlare.
5+
*
6+
* OpenShadowFlare is free software: you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License as published by the Free
8+
* Software Foundation, either version 3 of the License, or (at your option)
9+
* any later version.
10+
*
11+
* OpenShadowFlare is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14+
* details.
15+
*
16+
* You should have received a copy of the GNU General Public License along
17+
* with OpenShadowFlare. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
#include "game/generic_effect_actor.h"
21+
22+
#include "game/movement.h"
23+
24+
#include <limits.h>
25+
#include <string.h>
26+
27+
static int32_t sf_generic_effect_resource(int32_t effect_number) {
28+
static const int32_t resources[8] = {0, 1, 2, 3, 0, 0, 4, 0};
29+
return effect_number >= 0 && effect_number < 8
30+
? resources[effect_number] : -1;
31+
}
32+
33+
static SfObjectBounds sf_generic_effect_judgement(int32_t effect_number) {
34+
if (effect_number == 6)
35+
return (SfObjectBounds) {-160, -160, 159, 159};
36+
return (SfObjectBounds) {-30, -30, 30, 30};
37+
}
38+
39+
static bool sf_generic_effect_position(
40+
const SfCombatEffectRequest *request, SfWorldPoint resolved_source,
41+
SfWorldPoint *position) {
42+
SfWorldPoint offset;
43+
int64_t x;
44+
int64_t y;
45+
if (request->owner_kind == 0 && request->has_explicit_origin) {
46+
*position = request->origin;
47+
return true;
48+
}
49+
if (request->constructor_values_16_to_22[
50+
SF_COMBAT_EFFECT_CONSTRUCTOR_21] < 0) return false;
51+
if (!sf_movement_vector_at_distance(
52+
request->direction_vector,
53+
(uint32_t) request->constructor_values_16_to_22[
54+
SF_COMBAT_EFFECT_CONSTRUCTOR_21], &offset)) return false;
55+
x = (int64_t) resolved_source.x + offset.x;
56+
y = (int64_t) resolved_source.y + offset.y;
57+
if (x < INT32_MIN || x > INT32_MAX ||
58+
y < INT32_MIN || y > INT32_MAX) return false;
59+
*position = (SfWorldPoint) {(int32_t) x, (int32_t) y};
60+
return true;
61+
}
62+
63+
bool sf_generic_effect_actor_build(
64+
const SfCombatEffectRequest *request, SfWorldPoint resolved_source,
65+
SfGenericEffectActor *actor) {
66+
const SfWorldPoint zero = {0, 0};
67+
int32_t resource;
68+
if (!actor) return false;
69+
memset(actor, 0, sizeof(*actor));
70+
if (!request || !request->valid) return false;
71+
resource = sf_generic_effect_resource(request->effect_number);
72+
if (resource < 0 || request->effect_number == 2) return false;
73+
if (!sf_generic_effect_position(request, resolved_source, &actor->position))
74+
return false;
75+
actor->packet = request->packet;
76+
actor->direction_vector = request->direction_vector;
77+
actor->judgement = sf_generic_effect_judgement(request->effect_number);
78+
actor->effect_number = request->effect_number;
79+
actor->resource_id = resource;
80+
actor->owner_kind = request->owner_kind;
81+
actor->source_character_number = request->source_character_number;
82+
actor->target_kind = request->target_kind;
83+
actor->target_identifier = request->target_identifier;
84+
actor->travel_speed = request->constructor_value_6;
85+
actor->display_height = request->constructor_value_7;
86+
actor->lifetime = -1;
87+
actor->target_collision_start = 0;
88+
actor->contact_effect_number = request->effect_number == 6 ? 21023 : -1;
89+
actor->animation_chart = 0;
90+
actor->animation_direction = request->effect_number == 1 ? 8 :
91+
request->packet_kind == 8
92+
? sf_movement_direction(zero, request->direction_vector)
93+
: request->packet_kind;
94+
actor->target_audio_sample = 20u;
95+
actor->valid = true;
96+
actor->home_toward_target =
97+
request->constructor_values_16_to_22[
98+
SF_COMBAT_EFFECT_CONSTRUCTOR_20] == 1 && request->effect_number != 1;
99+
actor->collide_with_environment = true;
100+
actor->expire_on_environment_collision = true;
101+
actor->expire_on_target = true;
102+
actor->remember_targets = request->constructor_values_16_to_22[
103+
SF_COMBAT_EFFECT_CONSTRUCTOR_22] == 1;
104+
actor->has_packet = request->has_packet;
105+
return true;
106+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2026 Michael Binder and contributors
3+
*
4+
* This file is part of OpenShadowFlare.
5+
*
6+
* OpenShadowFlare is free software: you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License as published by the Free
8+
* Software Foundation, either version 3 of the License, or (at your option)
9+
* any later version.
10+
*
11+
* OpenShadowFlare is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14+
* details.
15+
*
16+
* You should have received a copy of the GNU General Public License along
17+
* with OpenShadowFlare. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
#ifndef SHADOWFLARE_GAME_GENERIC_EFFECT_ACTOR_H
21+
#define SHADOWFLARE_GAME_GENERIC_EFFECT_ACTOR_H
22+
23+
#include "core/bounds.h"
24+
#include "core/coordinates.h"
25+
#include "game/combat_effect_request.h"
26+
#include "game/combat_packet.h"
27+
28+
#include <stdbool.h>
29+
#include <stdint.h>
30+
31+
typedef struct SfGenericEffectActor {
32+
SfCombatPacket packet;
33+
SfWorldPoint position;
34+
SfWorldPoint direction_vector;
35+
SfObjectBounds judgement;
36+
int32_t effect_number;
37+
int32_t resource_id;
38+
int32_t owner_kind;
39+
int32_t source_character_number;
40+
int32_t target_kind;
41+
int32_t target_identifier;
42+
int32_t travel_speed;
43+
int32_t display_height;
44+
int32_t lifetime;
45+
int32_t target_collision_start;
46+
int32_t contact_effect_number;
47+
int32_t animation_chart;
48+
int32_t animation_direction;
49+
uint16_t target_audio_sample;
50+
bool valid;
51+
bool home_toward_target;
52+
bool collide_with_environment;
53+
bool expire_on_environment_collision;
54+
bool expire_on_target;
55+
bool remember_targets;
56+
bool has_packet;
57+
} SfGenericEffectActor;
58+
59+
bool sf_generic_effect_actor_build(
60+
const SfCombatEffectRequest *request, SfWorldPoint resolved_source,
61+
SfGenericEffectActor *actor);
62+
63+
#endif

shadowflare/game/movement.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ bool sf_movement_point_at_distance(
115115
return true;
116116
}
117117

118+
bool sf_movement_vector_at_distance(
119+
SfWorldPoint direction, uint32_t distance, SfWorldPoint *result) {
120+
return sf_movement_point_at_distance(
121+
(SfWorldPoint) {0, 0}, direction, distance, result);
122+
}
123+
118124
static int64_t sf_separated_edge_distance(
119125
int64_t first_start, int64_t first_end,
120126
int64_t second_start, int64_t second_end) {

shadowflare/game/movement.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ int32_t sf_movement_point_distance(SfWorldPoint first, SfWorldPoint second);
3939
bool sf_movement_point_at_distance(
4040
SfWorldPoint from, SfWorldPoint toward, uint32_t distance,
4141
SfWorldPoint *result);
42+
bool sf_movement_vector_at_distance(
43+
SfWorldPoint direction, uint32_t distance, SfWorldPoint *result);
4244
int32_t sf_movement_bounds_distance(
4345
SfWorldPoint first_position, SfObjectBounds first_bounds,
4446
SfWorldPoint second_position, SfObjectBounds second_bounds);

0 commit comments

Comments
 (0)