feat: port weapon fire sub-functions to Rust — Drill, Prod, vtable calls
Browse filesReplace naked asm bridges with typed Rust implementations:
- fire_worm_vtable_0xe → (*worm).set_state() for 8 weapons (9 call sites)
- Teleport check (0x516930) → pure Rust can_teleport() helper
- Drill fire (0x51E3E0) → pure Rust fire_drill(), fixes latent ECX bug
where bridge didn't set ECX=local_struct
- Prod fire (0x51E480) → pure Rust fire_prod() with trig interpolation;
dy must be negated (original asm uses IMUL -0x66666667)
- Add typed SpecialImpact wrapper (stdcall, 13 params)
- Delete 3 naked asm bridges: fire_worm_vtable_0xe,
call_fire_usercall_ret, call_fire_usercall_stdcall1
Add 3 replay tests: drill_prod_rope (Drill×4, Prod×3),
kamikaze (Kamikaze, Suicide Bomber),
various_weapons_1 (Bat, Blowtorch, Scales×3, Dragon Ball×2, more)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-wormkit/src/replacements/weapon.rs +176 -89
- testdata/replays/drill_prod_rope.WAgame +3 -0
- testdata/replays/drill_prod_rope_expected.log +3 -0
- testdata/replays/kamikaze.WAgame +3 -0
- testdata/replays/kamikaze_expected.log +3 -0
- testdata/replays/various_weapons_1.WAgame +3 -0
- testdata/replays/various_weapons_1_expected.log +3 -0
|
@@ -357,40 +357,66 @@ unsafe fn fire_weapon_special(
|
|
| 357 |
let params_38_ptr = &raw const (*entry).fire_subtype_38 as *const WeaponFireParams;
|
| 358 |
|
| 359 |
match subtype {
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
16 => {
|
| 373 |
-
// Teleport: MOV EAX,[ESI+0x44] (worm state) before check
|
| 374 |
let worm_state = (*worm).state();
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
call_fire_usercall(result as *const (), worm, rb(0x51EB00));
|
| 378 |
} else {
|
| 379 |
-
|
| 380 |
}
|
| 381 |
}
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
_ => {}
|
| 391 |
}
|
| 392 |
}
|
| 393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
// ── Pure Rust fire handlers (no bridge needed) ──────────────
|
| 395 |
|
| 396 |
/// Look up the CTaskTeam entity for a worm via SharedData.
|
|
@@ -515,7 +541,7 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
|
|
| 515 |
&& (worm_state == 0x7D || (worm_state == 0x78 && version < 8)));
|
| 516 |
|
| 517 |
if should_call_vtable {
|
| 518 |
-
|
| 519 |
}
|
| 520 |
|
| 521 |
fire_send_team_message(worm, 0x28);
|
|
@@ -527,6 +553,130 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
|
|
| 527 |
entry.turn_action_counter_Maybe += 7;
|
| 528 |
}
|
| 529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
// ── Naked asm bridges ───────────────────────────────────────
|
| 531 |
|
| 532 |
/// Bridge: ProjectileFire_Single — usercall(EDI=spawn_data, stack=[worm, fire_params]), RET 0x8.
|
|
@@ -596,69 +746,6 @@ unsafe extern "C" fn call_fire_usercall(_eax: *const (), _worm: *mut CTaskWorm,
|
|
| 596 |
);
|
| 597 |
}
|
| 598 |
|
| 599 |
-
/// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET, returns EAX.
|
| 600 |
-
#[unsafe(naked)]
|
| 601 |
-
unsafe extern "C" fn call_fire_usercall_ret(_eax: *const (), _worm: *mut CTaskWorm, _addr: u32) -> i32 {
|
| 602 |
-
core::arch::naked_asm!(
|
| 603 |
-
"push ebx",
|
| 604 |
-
"push esi",
|
| 605 |
-
"push edi",
|
| 606 |
-
"mov eax, [esp+16]",
|
| 607 |
-
"mov esi, [esp+20]",
|
| 608 |
-
"mov edi, [esp+20]",
|
| 609 |
-
"mov ebx, [esp+24]",
|
| 610 |
-
"call ebx",
|
| 611 |
-
"pop edi",
|
| 612 |
-
"pop esi",
|
| 613 |
-
"pop ebx",
|
| 614 |
-
"ret",
|
| 615 |
-
);
|
| 616 |
-
}
|
| 617 |
-
|
| 618 |
-
/// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm) + stdcall(1 param), RET 0x4.
|
| 619 |
-
#[unsafe(naked)]
|
| 620 |
-
unsafe extern "C" fn call_fire_usercall_stdcall1(
|
| 621 |
-
_eax: *const (), _worm: *mut CTaskWorm, _param: *const u8, _addr: u32,
|
| 622 |
-
) {
|
| 623 |
-
core::arch::naked_asm!(
|
| 624 |
-
"push ebx",
|
| 625 |
-
"push esi",
|
| 626 |
-
"push edi",
|
| 627 |
-
"mov eax, [esp+16]", // eax_val
|
| 628 |
-
"mov esi, [esp+20]", // worm
|
| 629 |
-
"mov edi, [esp+20]",
|
| 630 |
-
"mov ebx, [esp+28]", // addr (3 saves=12 + ret=4 + 3 args=12 = 28)
|
| 631 |
-
"push [esp+24]", // param
|
| 632 |
-
"call ebx",
|
| 633 |
-
"pop edi",
|
| 634 |
-
"pop esi",
|
| 635 |
-
"pop ebx",
|
| 636 |
-
"ret",
|
| 637 |
-
);
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
-
/// Call worm->vtable[0xE](msg_id) — thiscall(ECX=worm, stack=msg_id), RET 0x4.
|
| 641 |
-
/// Original: `(**(code **)(*worm + 0x38))(msg_id)`.
|
| 642 |
-
/// Uses naked bridge to avoid LLVM stack tracking issues with push/RET mismatch.
|
| 643 |
-
#[unsafe(naked)]
|
| 644 |
-
unsafe extern "C" fn fire_worm_vtable_0xe(_worm: *mut CTaskWorm, _msg_id: u32) {
|
| 645 |
-
core::arch::naked_asm!(
|
| 646 |
-
"push ebx",
|
| 647 |
-
"push esi",
|
| 648 |
-
"push edi",
|
| 649 |
-
"mov ecx, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
|
| 650 |
-
"mov esi, [esp+16]", // ESI = worm
|
| 651 |
-
"mov edi, [esp+16]", // EDI = worm
|
| 652 |
-
"mov ebx, [ecx]", // vtable
|
| 653 |
-
"mov ebx, [ebx+0x38]",// vtable[0xE]
|
| 654 |
-
"push [esp+20]", // msg_id
|
| 655 |
-
"call ebx", // thiscall: ECX=worm, RET 0x4 cleans msg_id
|
| 656 |
-
"pop edi",
|
| 657 |
-
"pop esi",
|
| 658 |
-
"pop ebx",
|
| 659 |
-
"ret",
|
| 660 |
-
);
|
| 661 |
-
}
|
| 662 |
|
| 663 |
// ============================================================
|
| 664 |
// Passthrough hooks — fire sub-functions
|
|
|
|
| 357 |
let params_38_ptr = &raw const (*entry).fire_subtype_38 as *const WeaponFireParams;
|
| 358 |
|
| 359 |
match subtype {
|
| 360 |
+
// Blowtorch
|
| 361 |
+
1 => (*worm).set_state(0x6C),
|
| 362 |
+
// Pneumatic Drill (pure Rust)
|
| 363 |
+
2 => fire_drill(worm, local_struct),
|
| 364 |
+
// Girder
|
| 365 |
+
3 => call_fire_stdcall3(worm, params_38_ptr, local_struct, rb(0x51E350)),
|
| 366 |
+
// Baseball Bat
|
| 367 |
+
4 => (*worm).set_state(0x6D),
|
| 368 |
+
// Fire Punch
|
| 369 |
+
5 => (*worm).set_state(0x75),
|
| 370 |
+
// Dragon Ball
|
| 371 |
+
6 => (*worm).set_state(0x70),
|
| 372 |
+
// Kamikaze
|
| 373 |
+
8 => (*worm).set_state(0x6E),
|
| 374 |
+
// Prod (pure Rust)
|
| 375 |
+
9 => fire_prod(worm, local_struct),
|
| 376 |
+
// Air Strike (EAX=worm)
|
| 377 |
+
10 => call_fire_usercall(worm as *const (), worm, rb(0x51E710)),
|
| 378 |
+
// Scales of Justice
|
| 379 |
+
11 => (*worm).set_state(0x71),
|
| 380 |
+
// Napalm Strike
|
| 381 |
+
13 => fire_send_team_message(worm, 0x2B),
|
| 382 |
+
// Mail/Mine/Mole (pure Rust)
|
| 383 |
+
14 => fire_mail_mine_mole(worm),
|
| 384 |
+
// Teleport: check worm state, then execute or cancel
|
| 385 |
16 => {
|
|
|
|
| 386 |
let worm_state = (*worm).state();
|
| 387 |
+
if can_teleport(worm_state) {
|
| 388 |
+
call_fire_usercall(worm_state as *const (), worm, rb(0x51EB00));
|
|
|
|
| 389 |
} else {
|
| 390 |
+
(*worm).set_state(0x74);
|
| 391 |
}
|
| 392 |
}
|
| 393 |
+
// Freeze (EAX=worm)
|
| 394 |
+
17 => call_fire_usercall(worm as *const (), worm, rb(0x51E920)),
|
| 395 |
+
// Suicide Bomber
|
| 396 |
+
18 => (*worm).set_state(0x72),
|
| 397 |
+
// Skip Go (pure Rust)
|
| 398 |
+
19 => fire_skip_go(worm, entry),
|
| 399 |
+
// Surrender (pure Rust)
|
| 400 |
+
20 => fire_surrender(worm),
|
| 401 |
+
// Select Worm (pure Rust)
|
| 402 |
+
21 => fire_select_worm(worm),
|
| 403 |
+
// Jet Pack (EAX=entry)
|
| 404 |
+
22 => call_fire_usercall(entry as *const (), worm, rb(0x51EC30)),
|
| 405 |
+
// Magic Bullet
|
| 406 |
+
23 => (*worm).set_state(0x78),
|
| 407 |
+
// Low Gravity (EAX=entry)
|
| 408 |
+
24 => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
|
| 409 |
_ => {}
|
| 410 |
}
|
| 411 |
}
|
| 412 |
|
| 413 |
+
/// Teleport validity check — pure Rust port of 0x516930.
|
| 414 |
+
/// Returns true if the worm's current state allows teleportation.
|
| 415 |
+
/// Valid states: 0x78 (idle-aim), 0x7B-0x7D (pre-fire range).
|
| 416 |
+
fn can_teleport(state: u32) -> bool {
|
| 417 |
+
state == 0x78 || (0x7B..=0x7D).contains(&state)
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
// ── Pure Rust fire handlers (no bridge needed) ──────────────
|
| 421 |
|
| 422 |
/// Look up the CTaskTeam entity for a worm via SharedData.
|
|
|
|
| 541 |
&& (worm_state == 0x7D || (worm_state == 0x78 && version < 8)));
|
| 542 |
|
| 543 |
if should_call_vtable {
|
| 544 |
+
(*worm).set_state(0x65);
|
| 545 |
}
|
| 546 |
|
| 547 |
fire_send_team_message(worm, 0x28);
|
|
|
|
| 553 |
entry.turn_action_counter_Maybe += 7;
|
| 554 |
}
|
| 555 |
|
| 556 |
+
// ── SpecialImpact wrapper ─────────────────────────────────
|
| 557 |
+
|
| 558 |
+
/// Typed wrapper for SpecialImpact (0x5193D0).
|
| 559 |
+
/// Convention: stdcall with 13 params (RET 0x34 = 52 bytes).
|
| 560 |
+
///
|
| 561 |
+
/// Used by Drill, Prod, and other melee/impact weapons. Applies damage in
|
| 562 |
+
/// a directional area from the worm's position.
|
| 563 |
+
#[allow(clippy::too_many_arguments)]
|
| 564 |
+
unsafe fn call_special_impact(
|
| 565 |
+
worm: *mut CTaskWorm,
|
| 566 |
+
x: i32, y: i32,
|
| 567 |
+
radius_x: i32, radius_y: i32,
|
| 568 |
+
weapon_type: i32,
|
| 569 |
+
dx: i32, dy: i32,
|
| 570 |
+
p8: i32, p9: i32, p10: i32,
|
| 571 |
+
flags: u32, p12: i32,
|
| 572 |
+
) {
|
| 573 |
+
use openwa_core::rebase::rb;
|
| 574 |
+
type SpecialImpactFn = unsafe extern "stdcall" fn(
|
| 575 |
+
*mut CTaskWorm, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, u32, i32,
|
| 576 |
+
);
|
| 577 |
+
let func: SpecialImpactFn = core::mem::transmute(rb(va::SPECIAL_IMPACT));
|
| 578 |
+
func(worm, x, y, radius_x, radius_y, weapon_type, dx, dy, p8, p9, p10, flags, p12);
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/// Compute version-dependent flags for SpecialImpact.
|
| 582 |
+
///
|
| 583 |
+
/// Pattern shared by Drill and Prod:
|
| 584 |
+
/// - Base flags OR 0x20 if version >= 2
|
| 585 |
+
/// - Base flags OR 0x10 if version >= 8
|
| 586 |
+
fn special_impact_version_flags(base: u32, version: u8) -> u32 {
|
| 587 |
+
let mut flags = base;
|
| 588 |
+
if version >= 2 { flags |= 0x20; }
|
| 589 |
+
if version >= 8 { flags |= 0x10; }
|
| 590 |
+
flags
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
/// Pneumatic Drill (subtype 2) — pure Rust port of 0x51E3E0.
|
| 594 |
+
///
|
| 595 |
+
/// Calls SpecialImpact with facing-offset position and scaled direction.
|
| 596 |
+
/// The original is usercall(ECX=local_struct, ESI=worm) — the old bridge
|
| 597 |
+
/// did not set ECX, so this port also fixes a latent bug.
|
| 598 |
+
unsafe fn fire_drill(worm: *mut CTaskWorm, local_struct: *const u8) {
|
| 599 |
+
let ddgame = (*worm).ddgame();
|
| 600 |
+
let version = (*ddgame).version_flag_4;
|
| 601 |
+
let entry = &*(*worm).active_weapon_entry;
|
| 602 |
+
let shot_count = entry.fire_params.shot_count;
|
| 603 |
+
let weapon_type = entry.fire_subtype_38;
|
| 604 |
+
let facing = (*worm).facing_direction_2;
|
| 605 |
+
|
| 606 |
+
// Cast local_struct to WeaponSpawnData for field access (offsets match)
|
| 607 |
+
let spawn = &*(local_struct as *const WeaponSpawnData);
|
| 608 |
+
|
| 609 |
+
let x = facing * 0x1A_0000 + spawn.spawn_x.0;
|
| 610 |
+
let y = spawn.spawn_y.0;
|
| 611 |
+
let dx = (spawn.initial_speed_x.0 * shot_count) / 10;
|
| 612 |
+
let dy = (spawn.initial_speed_y.0 * shot_count) / 10;
|
| 613 |
+
let flags = special_impact_version_flags(0x21C4C, version);
|
| 614 |
+
|
| 615 |
+
call_special_impact(
|
| 616 |
+
worm, x, y,
|
| 617 |
+
0x1A_0000, 0x1E_0000,
|
| 618 |
+
weapon_type, dx, dy,
|
| 619 |
+
6, 0x61, 0x51,
|
| 620 |
+
flags, 1,
|
| 621 |
+
);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
/// Prod (subtype 9) — pure Rust port of 0x51E480.
|
| 625 |
+
///
|
| 626 |
+
/// Like Drill but with trig interpolation on the spread angle.
|
| 627 |
+
/// Convention: usercall(EDI=worm) + 1 stack param (local_struct), RET 0x4.
|
| 628 |
+
unsafe fn fire_prod(worm: *mut CTaskWorm, local_struct: *const u8) {
|
| 629 |
+
use openwa_core::rebase::rb;
|
| 630 |
+
|
| 631 |
+
let ddgame = (*worm).ddgame();
|
| 632 |
+
let version = (*ddgame).version_flag_4;
|
| 633 |
+
let entry = &*(*worm).active_weapon_entry;
|
| 634 |
+
let shot_count = entry.fire_params.shot_count;
|
| 635 |
+
let spread = entry.fire_params.spread;
|
| 636 |
+
let weapon_type = entry.fire_subtype_38;
|
| 637 |
+
let facing = (*worm).facing_direction_2;
|
| 638 |
+
|
| 639 |
+
let spawn = &*(local_struct as *const WeaponSpawnData);
|
| 640 |
+
|
| 641 |
+
// Convert spread (degrees) to engine angle units: (spread << 16) / 360
|
| 642 |
+
let angle = ((spread as u32) << 16) / 0x168;
|
| 643 |
+
|
| 644 |
+
// Interpolated sin/cos lookup (same pattern as projectile_fire_impl)
|
| 645 |
+
let sin_table = rb(va::SIN_TABLE) as *const i32;
|
| 646 |
+
let cos_table = sin_table.add(256);
|
| 647 |
+
|
| 648 |
+
let table_index = ((angle >> 6) & 0x3FF) as usize;
|
| 649 |
+
let frac = ((angle & 0x3F) << 10) as i32;
|
| 650 |
+
|
| 651 |
+
let cos_base = *cos_table.add(table_index);
|
| 652 |
+
let cos_next = *cos_table.add(table_index + 1);
|
| 653 |
+
let cos_val = cos_base + fixed_mul(cos_next - cos_base, frac);
|
| 654 |
+
|
| 655 |
+
let sin_base = *sin_table.add(table_index);
|
| 656 |
+
let sin_next = *sin_table.add(table_index + 1);
|
| 657 |
+
let sin_val = sin_base + fixed_mul(sin_next - sin_base, frac);
|
| 658 |
+
|
| 659 |
+
// Scale trig results by shot_count and divide by 10.
|
| 660 |
+
// In WA's coordinate system (Y increases downward), the vertical component is negated.
|
| 661 |
+
// Original asm uses IMUL 0x99999999 (= -0x66666667) for dy, which negates the result.
|
| 662 |
+
// dx = (sin_val * shot_count) / 10 * facing
|
| 663 |
+
// dy = -(cos_val * shot_count) / 10
|
| 664 |
+
let dx = ((sin_val * shot_count) / 10) * facing;
|
| 665 |
+
let dy = -((cos_val * shot_count) / 10);
|
| 666 |
+
|
| 667 |
+
let x = facing * 0x6_0000 + spawn.spawn_x.0;
|
| 668 |
+
let y = spawn.spawn_y.0;
|
| 669 |
+
let flags = special_impact_version_flags(0xC4C, version);
|
| 670 |
+
|
| 671 |
+
call_special_impact(
|
| 672 |
+
worm, x, y,
|
| 673 |
+
0xC_0000, 0xC_0000,
|
| 674 |
+
weapon_type, dx, dy,
|
| 675 |
+
0, 0, 0,
|
| 676 |
+
flags, 1,
|
| 677 |
+
);
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
// ── Naked asm bridges ───────────────────────────────────────
|
| 681 |
|
| 682 |
/// Bridge: ProjectileFire_Single — usercall(EDI=spawn_data, stack=[worm, fire_params]), RET 0x8.
|
|
|
|
| 746 |
);
|
| 747 |
}
|
| 748 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
|
| 750 |
// ============================================================
|
| 751 |
// Passthrough hooks — fire sub-functions
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:33aa87bf1ab7bb954ae21d5dc178cbb2a0196712fe5dd090b066b7a8b23f2a1b
|
| 3 |
+
size 20440
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0bf673c8d5273a8d0e67b930674e56cf16a8e06933fa7c1e642057b2d1cb0eae
|
| 3 |
+
size 2320
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:79f91bddf709c993f764eb01c505a36939b39f2d4d5eeadd844ff784bb394587
|
| 3 |
+
size 9613
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:99ed67cd8537e6c40f33a043f2fc56f7fd7081dfb685eb77abb881a6eea89201
|
| 3 |
+
size 1105
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:10cdf52df18cd2eaab7ade52aac50676eec14be7709aaa06213f6c90f4c8bbf5
|
| 3 |
+
size 21775
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0db0acb7d5aff0df01e8dbbacd283de94393515f76342e546acd8ca6ae24d9a7
|
| 3 |
+
size 2822
|