@pkmn/engine 0.1.0-dev.abd68573 → 0.1.0-dev.ac9225d5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -255,6 +255,8 @@ pub const Duration = packed struct(u16) {
255
255
 
256
256
  pub const Commit = enum { hit, miss, binding };
257
257
 
258
+ const uN = if (options.miss) u8 else u9;
259
+
258
260
  /// Tracks chance actions and their associated probability during a Generation I battle update when
259
261
  /// `options.chance` is enabled.
260
262
  pub fn Chance(comptime Rational: type) type {
@@ -291,15 +293,12 @@ pub fn Chance(comptime Rational: type) type {
291
293
  //
292
294
  // Finally, because each player's actions are processed sequentially we only need a single
293
295
  // shared structure to track this information (though it needs to be cleared appropriately)
294
- pending: if (showdown) struct {
295
- hit: bool = false,
296
- hit_probablity: u8 = 0,
297
- } else struct {
296
+ pending: if (showdown) struct {} else struct {
298
297
  crit: bool = false,
299
298
  crit_probablity: u8 = 0,
300
299
  damage_roll: u8 = 0,
301
300
  hit: bool = false,
302
- hit_probablity: u8 = 0,
301
+ hit_probablity: uN = 0,
303
302
  binding: u8 = 0,
304
303
  } = .{},
305
304
 
@@ -308,25 +307,29 @@ pub fn Chance(comptime Rational: type) type {
308
307
 
309
308
  /// Convenience helper to clear fields which typically should be cleared between updates.
310
309
  pub fn reset(self: *Self) void {
310
+ if (!enabled) return;
311
+
311
312
  self.probability.reset();
312
313
  self.actions.reset();
313
314
  self.pending = .{};
314
315
  }
315
316
 
316
317
  pub fn commit(self: *Self, player: Player, kind: Commit) Error!void {
318
+ if (!enabled) return;
319
+
320
+ assert(!showdown);
321
+
317
322
  var action = self.actions.get(player);
318
323
 
319
324
  // We need to handle binding specially because we still need to commit it when attacking
320
325
  // into a Pokémon that is immune
321
- if (!showdown) {
322
- if (kind != .miss and self.pending.binding != 0) {
323
- assert(action.duration == 0);
324
- action.duration = @intCast(self.pending.binding);
325
- assert(action.durations.binding == 0);
326
- action.durations.binding = 1;
327
- }
328
- if (kind == .binding) return;
329
- } else assert(kind != .binding);
326
+ if (kind != .miss and self.pending.binding != 0) {
327
+ assert(action.duration == 0);
328
+ action.duration = @intCast(self.pending.binding);
329
+ assert(action.durations.binding == 0);
330
+ action.durations.binding = 1;
331
+ }
332
+ if (kind == .binding) return;
330
333
 
331
334
  // Always commit the hit result if we make it here (commit won't be called at all if the
332
335
  // target is immune/behind a sub/etc)
@@ -335,19 +338,19 @@ pub fn Chance(comptime Rational: type) type {
335
338
  action.hit = if (self.pending.hit) .true else .false;
336
339
  }
337
340
 
338
- if (showdown) return;
341
+ // If the move actually lands we can commit any past critical hit / damage rolls. Note
342
+ // we can't rely on the presence or absence of a damage roll to determine whether a
343
+ // "critical hit" was spurious or not as moves that do 1 or less damage don't rol for
344
+ // damage but can still crit (instead, whether or not a roll is deemed to be a no-op is
345
+ // passed to the critical hit helper)
346
+ if (kind != .hit) return assert(!self.pending.crit or self.pending.crit_probablity > 0);
339
347
 
340
- // If the move actually lands we can commit any past critical hit / damage rolls. We
341
- // avoid updating anything if there wasn't a damage roll as any "critical hit" not tied
342
- // to a damage roll is actually a no-op
343
- if (kind == .hit and self.pending.damage_roll > 0) {
344
- assert(!self.pending.crit or self.pending.crit_probablity > 0);
345
-
346
- if (self.pending.crit_probablity != 0) {
347
- try self.probability.update(self.pending.crit_probablity, 256);
348
- action.critical_hit = if (self.pending.crit) .true else .false;
349
- }
348
+ if (self.pending.crit_probablity != 0) {
349
+ try self.probability.update(self.pending.crit_probablity, 256);
350
+ action.critical_hit = if (self.pending.crit) .true else .false;
351
+ }
350
352
 
353
+ if (self.pending.damage_roll > 0) {
351
354
  try self.probability.update(1, 39);
352
355
  action.damage = self.pending.damage_roll;
353
356
  }
@@ -398,12 +401,17 @@ pub fn Chance(comptime Rational: type) type {
398
401
  self.actions.p2.speed_tie = self.actions.p1.speed_tie;
399
402
  }
400
403
 
401
- pub fn hit(self: *Self, ok: bool, accuracy: u8) void {
404
+ pub fn hit(self: *Self, player: Player, ok: bool, accuracy: uN) Error!void {
402
405
  if (!enabled) return;
403
406
 
404
407
  const p = if (ok) accuracy else @as(u8, @intCast(256 - @as(u9, accuracy)));
405
- self.pending.hit = ok;
406
- self.pending.hit_probablity = p;
408
+ if (showdown) {
409
+ try self.probability.update(p, 256);
410
+ self.actions.get(player).hit = if (ok) .true else .false;
411
+ } else {
412
+ self.pending.hit = ok;
413
+ self.pending.hit_probablity = p;
414
+ }
407
415
  }
408
416
 
409
417
  pub fn criticalHit(self: *Self, player: Player, crit: bool, rate: u8) Error!void {
@@ -621,19 +629,25 @@ test "Chance.speedTie" {
621
629
  test "Chance.hit" {
622
630
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
623
631
 
624
- chance.hit(true, 229);
625
- try expectProbability(&chance.probability, 1, 1);
626
- try expectValue(Optional(bool).None, chance.actions.p1.hit);
627
- try chance.commit(.P1, .hit);
632
+ try chance.hit(.P1, true, 229);
633
+ if (!showdown) {
634
+ try expectProbability(&chance.probability, 1, 1);
635
+ try expectValue(Optional(bool).None, chance.actions.p1.hit);
636
+
637
+ try chance.commit(.P1, .hit);
638
+ }
628
639
  try expectValue(Optional(bool).true, chance.actions.p1.hit);
629
640
  try expectProbability(&chance.probability, 229, 256);
630
641
 
631
642
  chance.reset();
632
643
 
633
- chance.hit(false, 255);
634
- try expectProbability(&chance.probability, 1, 1);
635
- try expectValue(Optional(bool).None, chance.actions.p2.hit);
636
- try chance.commit(.P2, .miss);
644
+ try chance.hit(.P2, false, 255);
645
+ if (!showdown) {
646
+ try expectProbability(&chance.probability, 1, 1);
647
+ try expectValue(Optional(bool).None, chance.actions.p2.hit);
648
+
649
+ try chance.commit(.P2, .miss);
650
+ }
637
651
  try expectValue(Optional(bool).false, chance.actions.p2.hit);
638
652
  try expectProbability(&chance.probability, 1, 256);
639
653
  }
@@ -649,26 +663,18 @@ test "Chance.criticalHit" {
649
663
  try expectProbability(&chance.probability, 1, 1);
650
664
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
651
665
 
652
- try chance.commit(.P1, .hit);
653
- try expectProbability(&chance.probability, 1, 1);
654
- try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
655
-
656
- chance.hit(true, 128);
666
+ try chance.hit(.P1, true, 128);
657
667
 
658
- try chance.commit(.P1, .hit);
659
- try expectProbability(&chance.probability, 1, 2);
668
+ try expectProbability(&chance.probability, 1, 1);
660
669
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
661
670
 
662
- chance.probability.reset();
663
671
  try chance.damage(.P1, 217);
664
672
 
665
- try chance.commit(.P1, .miss);
666
- try expectProbability(&chance.probability, 1, 2);
673
+ try expectProbability(&chance.probability, 1, 1);
667
674
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
668
675
 
669
- chance.probability.reset();
670
-
671
676
  try chance.commit(.P1, .hit);
677
+
672
678
  try expectProbability(&chance.probability, 17, 19968); // (1/2) * (17/256) * (1/39)
673
679
  try expectValue(Optional(bool).true, chance.actions.p1.critical_hit);
674
680
  }
@@ -683,20 +689,14 @@ test "Chance.criticalHit" {
683
689
  try expectProbability(&chance.probability, 1, 1);
684
690
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
685
691
 
686
- try chance.commit(.P1, .hit);
687
- try expectProbability(&chance.probability, 1, 1);
688
- try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
689
-
690
- chance.hit(true, 128);
692
+ try chance.hit(.P1, true, 128);
691
693
 
692
- try chance.commit(.P1, .hit);
693
- try expectProbability(&chance.probability, 1, 2);
694
+ try expectProbability(&chance.probability, 1, 1);
694
695
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
695
696
 
696
- chance.probability.reset();
697
697
  try chance.damage(.P1, 217);
698
-
699
698
  try chance.commit(.P1, .hit);
699
+
700
700
  try expectProbability(&chance.probability, 251, 19968); // (1/2) * (251/256) * (1/39)
701
701
  try expectValue(Optional(bool).false, chance.actions.p1.critical_hit);
702
702
  }
@@ -713,11 +713,8 @@ test "Chance.damage" {
713
713
  try expectProbability(&chance.probability, 1, 1);
714
714
  try expectValue(0, chance.actions.p1.damage);
715
715
 
716
- try chance.commit(.P1, .miss);
717
- try expectProbability(&chance.probability, 1, 1);
718
- try expectValue(0, chance.actions.p1.damage);
719
-
720
716
  try chance.commit(.P1, .hit);
717
+
721
718
  try expectProbability(&chance.probability, 1, 39);
722
719
  try expectValue(217, chance.actions.p1.damage);
723
720
  }
@@ -807,6 +804,7 @@ test "Chance.duration" {
807
804
  if (!showdown) {
808
805
  try expectValue(0, chance.actions.p2.duration);
809
806
  try expectValue(0, chance.actions.p2.durations.binding);
807
+
810
808
  try chance.commit(.P2, .hit);
811
809
  }
812
810
  try expectValue(4, chance.actions.p2.duration);
@@ -989,8 +987,8 @@ const Null = struct {
989
987
  _ = .{ self, p1 };
990
988
  }
991
989
 
992
- pub fn hit(self: Null, ok: bool, accuracy: u8) void {
993
- _ = .{ self, ok, accuracy };
990
+ pub fn hit(self: Null, player: Player, ok: bool, accuracy: uN) Error!void {
991
+ _ = .{ self, player, ok, accuracy };
994
992
  }
995
993
 
996
994
  pub fn criticalHit(self: Null, player: Player, crit: bool, rate: u8) Error!void {
@@ -22,11 +22,11 @@ const expect = std.testing.expect;
22
22
 
23
23
  /// The minimum size in bytes required to hold all Generation I choice options.
24
24
  pub const MAX_CHOICES: usize = 9; // move 1..4, switch 2..6
25
- /// The optimal size in bytes required to hold all Generation I choice options.
26
- /// At least as large as MAX_CHOICES.
25
+ /// The maximum number of bytes possibly logged by a single Generation I update.
27
26
  pub const MAX_LOGS: usize = 180;
28
27
 
29
- /// The maximum number of bytes possibly logged by a single Generation I update.
28
+ /// The optimal size in bytes required to hold all Generation I choice options.
29
+ /// At least as large as MAX_CHOICES.
30
30
  pub const CHOICES_SIZE = if (builtin.mode == .ReleaseSmall)
31
31
  MAX_CHOICES
32
32
  else
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const builtin = @import("builtin");
2
3
 
3
4
  const common = @import("../common/data.zig");
4
5
  const optional = @import("../common/optional.zig");
@@ -35,6 +36,8 @@ const Status = data.Status;
35
36
  const Action = chance.Action;
36
37
  const Actions = chance.Actions;
37
38
 
39
+ const endian = builtin.cpu.arch.endian();
40
+
38
41
  /// Configuration options controlling the domain of valid Pokémon that can be generated by the
39
42
  /// random generator helpers.
40
43
  pub const Options = struct {
@@ -43,6 +46,8 @@ pub const Options = struct {
43
46
  /// Whether to generate Pokémon with moves that contain bugs on Pokémon Showdown that are
44
47
  /// unimplementable in the engine.
45
48
  block: bool = showdown,
49
+ // FIXME: remove once durations work with -Dchance
50
+ durations: bool = false,
46
51
  };
47
52
 
48
53
  /// Helpers to simplify initialization of a Generation I battle.
@@ -230,6 +235,7 @@ pub const Pokemon = struct {
230
235
  var m: Move = .None;
231
236
  sample: while (true) {
232
237
  m = @enumFromInt(rand.range(u8, 1, Move.size - 1 + 1));
238
+ if (opt.durations and durations(m)) continue :sample;
233
239
  if (opt.block and blocked(m)) continue :sample;
234
240
  for (0..i) |j| if (ms[j].id == m) continue :sample;
235
241
  break;
@@ -267,6 +273,23 @@ fn blocked(m: Move) bool {
267
273
  };
268
274
  }
269
275
 
276
+ fn durations(m: Move) bool {
277
+ // Any moves that have effects that result in duration tracking with -Dchance
278
+ return switch (Move.get(m).effect) {
279
+ .Confusion,
280
+ .Bide,
281
+ .Sleep,
282
+ .Rage,
283
+ .Binding,
284
+ .Thrashing,
285
+ .ConfusionChance,
286
+ .Disable,
287
+ .Metronome,
288
+ => true,
289
+ else => false,
290
+ };
291
+ }
292
+
270
293
  /// Convenience helper to create a move-type choice for the provided move slot.
271
294
  pub fn move(slot: u4) Choice {
272
295
  return .{ .type = .Move, .data = slot };
@@ -287,7 +310,7 @@ pub const Rolls = struct {
287
310
 
288
311
  /// Returns a slice with the correct range of values for speed ties given the `action` state.
289
312
  pub fn speedTie(action: Action) []const Optional(Player) {
290
- return if (@field(action, "speed_tie") == .None) &PLAYER_NONE else &PLAYERS;
313
+ return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
291
314
  }
292
315
 
293
316
  const BOOL_NONE = [_]Optional(bool){.None};
@@ -297,25 +320,19 @@ pub const Rolls = struct {
297
320
  /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
298
321
  pub fn hit(action: Action, parent: Optional(bool)) []const Optional(bool) {
299
322
  if (parent == .true) return &BOOL_NONE;
300
- return if (@field(action, "hit") == .None) &BOOL_NONE else &BOOLS;
323
+ return if (action.hit == .None) &BOOL_NONE else &BOOLS;
301
324
  }
302
325
 
303
326
  /// Returns a slice with the correct range of values for critical hits given the `action` state
304
327
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
305
- pub fn criticalHit(
306
- action: Action,
307
- parent: Optional(bool),
308
- ) []const Optional(bool) {
328
+ pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
309
329
  if (parent == .false) return &BOOL_NONE;
310
- return if (@field(action, "critical_hit") == .None) &BOOL_NONE else &BOOLS;
330
+ return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
311
331
  }
312
332
 
313
333
  /// Returns a slice with the correct range of values for secondary chances hits given the
314
334
  /// `action` state and the state of the `parent` (whether the player's Pokémon's move hit).
315
- pub fn secondaryChance(
316
- action: Action,
317
- parent: Optional(bool),
318
- ) []const Optional(bool) {
335
+ pub fn secondaryChance(action: Action, parent: Optional(bool)) []const Optional(bool) {
319
336
  if (parent == .false) return &BOOL_NONE;
320
337
  return if (@field(action, "secondary_chance") == .None) &BOOL_NONE else &BOOLS;
321
338
  }
@@ -326,7 +343,7 @@ pub const Rolls = struct {
326
343
  /// Returns the range bounding damage rolls given the `action` state and the state of
327
344
  /// the `parent` (whether the player's Pokémon's move hit).
328
345
  pub fn damage(action: Action, parent: Optional(bool)) Range {
329
- return if (parent == .false or @field(action, "damage") == 0)
346
+ return if (parent == .false or action.damage == 0)
330
347
  .{ .min = 0, .max = 1 }
331
348
  else
332
349
  .{ .min = 217, .max = 256 };
@@ -347,14 +364,14 @@ pub const Rolls = struct {
347
364
  /// Returns a slice with the correct range of values for confused given the `action` state
348
365
  /// and the state of the `parent` (the player's Pokémon's remaining confusion turns).
349
366
  pub fn confused(action: Action, parent: u4) []const Optional(bool) {
350
- return if (parent == 0 or @field(action, "confused") == .None) &BOOL_NONE else &BOOLS;
367
+ return if (parent == 0 or action.confused == .None) &BOOL_NONE else &BOOLS;
351
368
  }
352
369
 
353
370
  /// Returns a slice with the correct range of values for paralysis given the `action` state
354
371
  /// and the state of the `parent` (whether the player's Pokémon was confused).
355
372
  pub fn paralyzed(action: Action, parent: Optional(bool)) []const Optional(bool) {
356
373
  if (parent == .true) return &BOOL_NONE;
357
- return if (@field(action, "paralyzed") == .None) &BOOL_NONE else &BOOLS;
374
+ return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
358
375
  }
359
376
 
360
377
  const DURATION_NONE = [_]u1{0};
@@ -364,7 +381,7 @@ pub const Rolls = struct {
364
381
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
365
382
  pub fn duration(action: Action, parent: Optional(bool)) []const u1 {
366
383
  if (parent == .false) return &DURATION_NONE;
367
- return if (@field(action, "duration") == 0) &DURATION_NONE else &DURATION;
384
+ return if (action.duration == 0) &DURATION_NONE else &DURATION;
368
385
  }
369
386
 
370
387
  const U3_NONE = [_]u3{0};
@@ -372,8 +389,8 @@ pub const Rolls = struct {
372
389
 
373
390
  /// Returns a slice with a range of values for sleep given the `action` and `origin` states.
374
391
  pub inline fn sleep(action: Action, origin: Action) []const u3 {
375
- const a = @field(action.durations, "sleep");
376
- const o = @field(origin.durations, "sleep");
392
+ const a = action.durations.sleep;
393
+ const o = origin.durations.sleep;
377
394
  if (o == a) return if (o == 0) &U3_NONE else &[_]u3{o};
378
395
  assert(a == 0 or a == o + 1);
379
396
  return if (o >= 7) &U3_NONE else &[_]u3{ 0, o + 1 };
@@ -385,8 +402,8 @@ pub const Rolls = struct {
385
402
  /// and the state of the `parent` (the player's Pokémon's remaining sleep turns).
386
403
  pub inline fn disable(action: Action, origin: Action, parent: u4) []const u4 {
387
404
  if (parent > 0) return &DISABLE_NONE;
388
- const a = @field(action.durations, "disable");
389
- const o = @field(origin.durations, "disable");
405
+ const a = action.durations.disable;
406
+ const o = origin.durations.disable;
390
407
  if (o == a) return if (o == 0) &DISABLE_NONE else &[_]u4{o};
391
408
  assert(a == 0 or a == o + 1);
392
409
  return if (o >= 8) &DISABLE_NONE else &[_]u4{ 0, o + 1 };
@@ -396,8 +413,8 @@ pub const Rolls = struct {
396
413
  /// and the state of the `parent` (the player's remaining sleep turns).
397
414
  pub inline fn confusion(action: Action, origin: Action, parent: u4) []const u3 {
398
415
  if (parent > 0) return &U3_NONE;
399
- const a = @field(action.durations, "confusion");
400
- const o = @field(origin.durations, "confusion");
416
+ const a = action.durations.confusion;
417
+ const o = origin.durations.confusion;
401
418
  if (o == a) return if (o == 0) &U3_NONE else &[_]u3{o};
402
419
  assert(a == 0 or a == o + 1);
403
420
  return if (o >= 5) &U3_NONE else if (o < 2)
@@ -410,8 +427,8 @@ pub const Rolls = struct {
410
427
  /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
411
428
  pub inline fn attacking(action: Action, origin: Action, parent: Optional(bool)) []const u3 {
412
429
  if (parent == .true) return &U3_NONE;
413
- const a = @field(action.durations, "attacking");
414
- const o = @field(origin.durations, "attacking");
430
+ const a = action.durations.attacking;
431
+ const o = origin.durations.attacking;
415
432
  if (o == a) return if (o == 0) &U3_NONE else &[_]u3{o};
416
433
  assert(a == 0 or a == o + 1);
417
434
  return if (o >= 3) &U3_NONE else if (o < 2)
@@ -424,8 +441,8 @@ pub const Rolls = struct {
424
441
  /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
425
442
  pub inline fn binding(action: Action, origin: Action, parent: Optional(bool)) []const u3 {
426
443
  if (parent == .true) return &U3_NONE;
427
- const a = @field(action.durations, "binding");
428
- const o = @field(origin.durations, "binding");
444
+ const a = action.durations.binding;
445
+ const o = origin.durations.binding;
429
446
  if (o == a) return if (o == 0) &U3_NONE else &[_]u3{o};
430
447
  assert(a == 0 or a == o + 1);
431
448
  return if (o >= 4) &U3_NONE else &[_]u3{ 0, o + 1 };
@@ -440,7 +457,7 @@ pub const Rolls = struct {
440
457
  /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
441
458
  pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
442
459
  if (parent == .false) return &SLOT_NONE;
443
- return if (@field(action, "move_slot") == 0) &SLOT_NONE else &SLOT;
460
+ return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
444
461
  }
445
462
 
446
463
  const MULTI_NONE = [_]u4{0};
@@ -450,7 +467,7 @@ pub const Rolls = struct {
450
467
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
451
468
  pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
452
469
  if (parent == .false) return &MULTI_NONE;
453
- return if (@field(action, "multi_hit") == 0) &MULTI_NONE else &MULTI;
470
+ return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
454
471
  }
455
472
 
456
473
  const PSYWAVE_NONE = [_]u8{0};
@@ -468,7 +485,7 @@ pub const Rolls = struct {
468
485
  parent: Optional(bool),
469
486
  ) []const u8 {
470
487
  if (parent == .false) return &PSYWAVE_NONE;
471
- return if (@field(action, "psywave") == 0)
488
+ return if (action.psywave == 0)
472
489
  &PSYWAVE_NONE
473
490
  else
474
491
  PSYWAVE[0 .. @as(u16, side.stored().level) * 3 / 2];
@@ -478,7 +495,7 @@ pub const Rolls = struct {
478
495
 
479
496
  /// Returns a slice with the correct range of values for metronome given the `action` state.
480
497
  pub fn metronome(action: Action) []const Move {
481
- return if (@field(action, "metronome") == .None) &MOVE_NONE else &Move.METRONOME;
498
+ return if (action.metronome == .None) &MOVE_NONE else &Move.METRONOME;
482
499
  }
483
500
  };
484
501
 
@@ -797,3 +814,47 @@ test "Rolls.psywave" {
797
814
  try expectEqual(121, rolls[rolls.len - 1]);
798
815
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
799
816
  }
817
+
818
+ /// Empirically determined maximum number of bytes possibly required to store the diff between two
819
+ /// Generation I battles.
820
+ pub const MAX_DIFFS: usize = 117;
821
+
822
+ /// The optimal size in bytes required to hold the diff between two Generation I battles.
823
+ /// At least as large as MAX_DIFFS.
824
+ pub const DIFFS_SIZE = if (builtin.mode == .ReleaseSmall)
825
+ MAX_DIFFS
826
+ else
827
+ std.math.ceilPowerOfTwo(usize, MAX_DIFFS) catch unreachable;
828
+
829
+ /// TODO
830
+ pub fn diff(
831
+ a: *const data.Battle(data.PRNG),
832
+ b: *const data.Battle(data.PRNG),
833
+ w: protocol.ByteStream.Writer,
834
+ ) !usize {
835
+ const pos = w.stream.pos;
836
+ const size = @sizeOf(data.Battle(data.PRNG)) - @sizeOf(data.PRNG);
837
+ assert(size % 2 == 0);
838
+ assert(size / 2 < 255);
839
+
840
+ var i: usize = 0;
841
+ while (i < size) : (i += 2) {
842
+ const val = std.mem.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
843
+ if (val != std.mem.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
844
+ try w.writeByte(@intCast(i / 2));
845
+ try w.writeInt(u16, val, endian);
846
+ }
847
+ }
848
+
849
+ return w.stream.pos - pos;
850
+ }
851
+
852
+ /// TODO
853
+ pub fn patch(battle: *data.Battle(data.PRNG), buf: []u8) void {
854
+ var i: usize = 0;
855
+ while (i < buf.len) : (i += 3) {
856
+ const off = @as(u16, @intCast(buf[i])) * 2;
857
+ const val = std.mem.readInt(u16, @ptrCast(buf[i + 1 .. i + 3]), endian);
858
+ std.mem.writeInt(u16, @ptrCast(std.mem.asBytes(battle)[off .. off + 2]), val, endian);
859
+ }
860
+ }