From 74d1e0e89b1946fe3d24296016f9cc3bfdaa367f Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 15 Oct 2026 00:56:12 -0400 Subject: [PATCH] Add the Warden VALORANT's new rifle. It goes last in WeaponType so every saved index stays put (Hive index 20), and lists after the Vandal under Rifles. The icon is Valorant-API's kill-feed silhouette, like the other guns, and replays map the game's /Rifles/BattleRifle/ folder to it. Desktop 4.6.3 doesn't know index 20 and reads it as no weapon. The rollback tests now use 4.6.3's own weapon adapter and prove a page with a Warden still opens there whole, with that agent unarmed. Co-Authored-By: Claude Opus 5.5 --- assets/weapons/README.md | 1 + assets/weapons/warden.png | Bin 0 -> 6659 bytes lib/const/placed_classes.g.dart | 1 + lib/const/weapons.dart | 3 +- lib/hive/hive_adapters.g.dart | 4 + lib/hive/hive_adapters.g.yaml | 4 +- lib/replay/replay_weapons.dart | 1 + .../draggable_widgets/agents/weapon_icon.dart | 1 + test/agent_weapon_widgets_test.dart | 2 +- test/desktop_4_6_3_adapters_test_support.dart | 116 +++++++++++++++++- test/hive_desktop_rollback_test.dart | 22 +++- 11 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 assets/weapons/warden.png diff --git a/assets/weapons/README.md b/assets/weapons/README.md index 720372f52..0c73edf49 100644 --- a/assets/weapons/README.md +++ b/assets/weapons/README.md @@ -25,3 +25,4 @@ The PNGs are unmodified and retain transparency. `WeaponIcon` fits their visible | marshal.png | [Marshal](https://media.valorant-api.com/weapons/c4883e50-4494-202c-3ec3-6b8a9284f00b/killstreamicon.png) | | spectre.png | [Spectre](https://media.valorant-api.com/weapons/462080d1-4035-2937-7c09-27aa2a5c27a7/killstreamicon.png) | | stinger.png | [Stinger](https://media.valorant-api.com/weapons/f7e1b454-4ad4-1063-ec0a-159e56b58941/killstreamicon.png) | +| warden.png | [Warden](https://media.valorant-api.com/weapons/8db0a1bf-4a50-832a-4566-faaaa6d250ca/killstreamicon.png), retrieved 2026-10-15 | diff --git a/assets/weapons/warden.png b/assets/weapons/warden.png new file mode 100644 index 0000000000000000000000000000000000000000..07b0ba4db6f1055f1172d3e9743bbcbd2cddaf1d GIT binary patch literal 6659 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 YVqfhXo#8qk^sf#u)HTMG>tOHyKY-MaN&o-= literal 0 HcmV?d00001 diff --git a/lib/const/placed_classes.g.dart b/lib/const/placed_classes.g.dart index e2aebc8d1..a0e2ac811 100644 --- a/lib/const/placed_classes.g.dart +++ b/lib/const/placed_classes.g.dart @@ -118,6 +118,7 @@ const _$WeaponTypeEnumMap = { WeaponType.operator: 'operator', WeaponType.ares: 'ares', WeaponType.odin: 'odin', + WeaponType.warden: 'warden', }; PlacedViewConeAgent _$PlacedViewConeAgentFromJson(Map json) => diff --git a/lib/const/weapons.dart b/lib/const/weapons.dart index ab4183cb5..a6a497035 100644 --- a/lib/const/weapons.dart +++ b/lib/const/weapons.dart @@ -20,7 +20,8 @@ enum WeaponType { outlaw('Outlaw', WeaponCategory.snipers), operator('Operator', WeaponCategory.snipers), ares('Ares', WeaponCategory.heavies), - odin('Odin', WeaponCategory.heavies); + odin('Odin', WeaponCategory.heavies), + warden('Warden', WeaponCategory.rifles); const WeaponType(this.displayName, this.category); diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index f76e34c02..c6f8537f2 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -1638,6 +1638,8 @@ class WeaponTypeAdapter extends TypeAdapter { return WeaponType.ares; case 19: return WeaponType.odin; + case 20: + return WeaponType.warden; default: return WeaponType.none; } @@ -1686,6 +1688,8 @@ class WeaponTypeAdapter extends TypeAdapter { writer.writeByte(18); case WeaponType.odin: writer.writeByte(19); + case WeaponType.warden: + writer.writeByte(20); } } diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index 0197e0409..1798dc010 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -539,7 +539,7 @@ types: index: 6 WeaponType: typeId: 38 - nextIndex: 20 + nextIndex: 21 fields: none: index: 0 @@ -581,6 +581,8 @@ types: index: 18 odin: index: 19 + warden: + index: 20 CloudMediaJobState: typeId: 39 nextIndex: 3 diff --git a/lib/replay/replay_weapons.dart b/lib/replay/replay_weapons.dart index 41f061239..4b31ddc69 100644 --- a/lib/replay/replay_weapons.dart +++ b/lib/replay/replay_weapons.dart @@ -18,6 +18,7 @@ const List<(String, WeaponType)> _weaponsByPathPart = [ ('/Rifles/DMR/', WeaponType.guardian), ('/Rifles/Carbine/', WeaponType.phantom), ('/Rifles/AK/', WeaponType.vandal), + ('/Rifles/BattleRifle/', WeaponType.warden), ('/Sniper/LeverSniperRifle/', WeaponType.marshal), ('/Sniper/DoubleSniper/', WeaponType.outlaw), ('/Sniper/BoltSniper/', WeaponType.operator), diff --git a/lib/widgets/draggable_widgets/agents/weapon_icon.dart b/lib/widgets/draggable_widgets/agents/weapon_icon.dart index 5a59e404a..565425261 100644 --- a/lib/widgets/draggable_widgets/agents/weapon_icon.dart +++ b/lib/widgets/draggable_widgets/agents/weapon_icon.dart @@ -96,4 +96,5 @@ const _artworkBounds = { WeaponType.operator: (Size(512, 96), Rect.fromLTRB(18, 0, 494, 95)), WeaponType.ares: (Size(512, 96), Rect.fromLTRB(18, 1, 495, 96)), WeaponType.odin: (Size(384, 96), Rect.fromLTRB(18, 1, 361, 96)), + WeaponType.warden: (Size(512, 140), Rect.fromLTRB(4, 4, 505, 137)), }; diff --git a/test/agent_weapon_widgets_test.dart b/test/agent_weapon_widgets_test.dart index 63a9f46b5..ee347762b 100644 --- a/test/agent_weapon_widgets_test.dart +++ b/test/agent_weapon_widgets_test.dart @@ -470,7 +470,7 @@ void main() { }); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); - expect(find.byType(WeaponIcon), findsNWidgets(19)); + expect(find.byType(WeaponIcon), findsNWidgets(20)); final sizes = tester .elementList(find.byType(AgentWidget)) .map( diff --git a/test/desktop_4_6_3_adapters_test_support.dart b/test/desktop_4_6_3_adapters_test_support.dart index 939eec4c8..27bd1eb03 100644 --- a/test/desktop_4_6_3_adapters_test_support.dart +++ b/test/desktop_4_6_3_adapters_test_support.dart @@ -1,6 +1,7 @@ // Desktop 4.6.3's adapters for the typeIds this build writes by hand, copied // verbatim from the desktop-stable-v4.6.3+103 tag (only the class names -// changed). Every other adapter is unchanged since that release. Rollback +// changed), plus its WeaponTypeAdapter, which this build extended with the +// Warden. Every other adapter is unchanged since that release. Rollback // tests decode with these to prove 4.6.3 opens, and keeps, what this build // saves. Never edit them to match this build. @@ -505,6 +506,119 @@ class Desktop463EllipseDrawingAdapter extends TypeAdapter { } } +class Desktop463WeaponTypeAdapter extends TypeAdapter { + @override + final typeId = 38; + + @override + WeaponType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return WeaponType.none; + case 1: + return WeaponType.classic; + case 2: + return WeaponType.shorty; + case 3: + return WeaponType.frenzy; + case 4: + return WeaponType.ghost; + case 5: + return WeaponType.bandit; + case 6: + return WeaponType.sheriff; + case 7: + return WeaponType.bulldog; + case 8: + return WeaponType.guardian; + case 9: + return WeaponType.phantom; + case 10: + return WeaponType.vandal; + case 11: + return WeaponType.bucky; + case 12: + return WeaponType.judge; + case 13: + return WeaponType.stinger; + case 14: + return WeaponType.spectre; + case 15: + return WeaponType.marshal; + case 16: + return WeaponType.outlaw; + case 17: + return WeaponType.operator; + case 18: + return WeaponType.ares; + case 19: + return WeaponType.odin; + default: + return WeaponType.none; + } + } + + @override + void write(BinaryWriter writer, WeaponType obj) { + switch (obj) { + case WeaponType.none: + writer.writeByte(0); + case WeaponType.classic: + writer.writeByte(1); + case WeaponType.shorty: + writer.writeByte(2); + case WeaponType.frenzy: + writer.writeByte(3); + case WeaponType.ghost: + writer.writeByte(4); + case WeaponType.bandit: + writer.writeByte(5); + case WeaponType.sheriff: + writer.writeByte(6); + case WeaponType.bulldog: + writer.writeByte(7); + case WeaponType.guardian: + writer.writeByte(8); + case WeaponType.phantom: + writer.writeByte(9); + case WeaponType.vandal: + writer.writeByte(10); + case WeaponType.bucky: + writer.writeByte(11); + case WeaponType.judge: + writer.writeByte(12); + case WeaponType.stinger: + writer.writeByte(13); + case WeaponType.spectre: + writer.writeByte(14); + case WeaponType.marshal: + writer.writeByte(15); + case WeaponType.outlaw: + writer.writeByte(16); + case WeaponType.operator: + writer.writeByte(17); + case WeaponType.ares: + writer.writeByte(18); + case WeaponType.odin: + writer.writeByte(19); + // Not in 4.6.3, which can never hold one; only Dart's exhaustive + // switch needs the case. + case WeaponType.warden: + throw StateError('Desktop 4.6.3 has no Warden'); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Desktop463WeaponTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + int _readDrawingHiveColorValue( Map fields, { required int colorFieldIndex, diff --git a/test/hive_desktop_rollback_test.dart b/test/hive_desktop_rollback_test.dart index b019d5f2b..da107ae5d 100644 --- a/test/hive_desktop_rollback_test.dart +++ b/test/hive_desktop_rollback_test.dart @@ -30,7 +30,8 @@ HiveImpl _currentRegistry() { } /// What desktop 4.6.3 registers: this build's adapters, which are unchanged -/// since that release, except the eight this build writes by hand. +/// since that release, except the eight this build writes by hand and the +/// weapon list, which gained the Warden. HiveImpl _desktop463Registry() { final registry = _currentRegistry(); // hive_ce warns on every override, which is the point here. @@ -45,7 +46,8 @@ HiveImpl _desktop463Registry() { ..registerAdapter(Desktop463FreeDrawingAdapter(), override: true) ..registerAdapter(Desktop463LineAdapter(), override: true) ..registerAdapter(Desktop463RectangleDrawingAdapter(), override: true) - ..registerAdapter(Desktop463EllipseDrawingAdapter(), override: true); + ..registerAdapter(Desktop463EllipseDrawingAdapter(), override: true) + ..registerAdapter(Desktop463WeaponTypeAdapter(), override: true); } finally { Logger.level = previousLevel; } @@ -191,6 +193,22 @@ void main() { expect(opened.lineUpOrigins.single.agent.weapon, WeaponType.sheriff); }); + test('desktop 4.6.3 opens a Warden this build saved as no weapon', () { + final page = _richPage(); + page.agentData.first.weapon = WeaponType.warden; + page.lineUpOrigins.single.agent.weapon = WeaponType.warden; + final opened = _across(current, desktop463, page); + + expect( + opened.agentData.map((agent) => agent.weapon), + [WeaponType.none, WeaponType.operator, WeaponType.none], + ); + expect(opened.lineUpOrigins.single.agent.weapon, WeaponType.none); + page.agentData.first.weapon = WeaponType.none; + page.lineUpOrigins.single.agent.weapon = WeaponType.none; + _expectSamePage(opened, page); + }); + test('what desktop 4.6.3 saves back, this build opens whole', () { final page = _richPage(); final rolledBack = _across(current, desktop463, page);