Expose retreating army state in server views (#8686)

* Expose retreating armies in progress panels

* Keep retreating army PR server-side
This commit is contained in:
2026-07-17 18:58:57 -07:00
committed by GitHub
parent fe4e205a07
commit e07cdc8123
28 changed files with 165 additions and 76 deletions
@@ -25,6 +25,32 @@
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
namespace shardok {
auto PlayerBattleProgressFrom(const net::eagle0::shardok::storage::fb::GameState &gameState)
-> std::vector<PlayerBattleProgress> {
std::vector<PlayerBattleProgress> progress;
progress.reserve(gameState.player_infos()->size());
for (const auto *player : *gameState.player_infos()) {
int32_t troopCount = 0;
bool isRetreating = false;
for (const auto *unit : *gameState.units()) {
if (unit->player_id() != player->player_id()) continue;
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
troopCount += unit->battalion().size();
break;
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
isRetreating = true;
break;
default: break;
}
}
progress.push_back(PlayerBattleProgress{player->player_id(), troopCount, isRetreating});
}
return progress;
}
using net::eagle0::common::ScopedShardokLatencyTrace;
using net::eagle0::shardok::common::GameStatus;
@@ -588,22 +614,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
// Extract battle progress data from the FlatBuffer game state
const auto *gs = engine->GetCurrentGameState().Get();
updates.currentRound = gs->current_round();
updates.playerTroopCounts.reserve(gs->player_infos()->size());
for (const auto *player : *gs->player_infos()) {
int32_t troopCount = 0;
for (const auto *unit : *gs->units()) {
if (unit->player_id() != player->player_id()) continue;
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
troopCount += unit->battalion().size();
break;
default: break;
}
}
updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
}
updates.playerBattleProgress = PlayerBattleProgressFrom(*gs);
}
if (recommendationEngine) {
@@ -724,7 +735,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
updates.newUnfilteredCount,
updates.currentGameState,
updates.currentRound,
updates.playerTroopCounts);
updates.playerBattleProgress);
}
// Tutorial battle reset: defender lost, signal reset instead of game over
@@ -59,6 +59,16 @@ struct OnePlayerUpdates {
availableCommands(acs) {}
};
struct PlayerBattleProgress {
int32_t playerId;
int32_t troopCount;
bool isRetreating;
};
[[nodiscard]] auto PlayerBattleProgressFrom(
const net::eagle0::shardok::storage::fb::GameState& gameState)
-> std::vector<PlayerBattleProgress>;
/// Result of WaitForUpdatesAndPush indicating how the wait ended
enum class WaitResult {
GAME_OVER, // Game ended normally
@@ -78,7 +88,7 @@ public:
int32_t newUnfilteredCount,
const byte_vector& currentGameState,
int32_t currentRound,
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
const std::vector<PlayerBattleProgress>& playerBattleProgress) = 0;
/// Called when the game ends
virtual void OnGameOver(const GameOverInfo& info) = 0;
@@ -189,7 +199,7 @@ public:
int32_t newUnfilteredCount;
byte_vector currentGameState;
int32_t currentRound = 0;
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
std::vector<PlayerBattleProgress> playerBattleProgress;
};
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
auto GetCurrentGameStateBytes() -> byte_vector;
@@ -420,10 +420,11 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
string(controller->GetCurrentGameStateBytes());
response->mutable_game_update_response()->set_current_round(results.currentRound);
for (const auto &[playerId, troopCount] : results.playerTroopCounts) {
for (const auto &[playerId, troopCount, isRetreating] : results.playerBattleProgress) {
auto *ptc = response->mutable_game_update_response()->add_player_troop_counts();
ptc->set_player_id(playerId);
ptc->set_troop_count(troopCount);
ptc->set_is_retreating(isRetreating);
}
}
}
@@ -539,7 +540,7 @@ auto EagleInterfaceImpl::SubscribeToGame(
int32_t newUnfilteredCount,
const byte_vector &currentGameState,
int32_t currentRound,
const vector<std::pair<int32_t, int32_t>> &playerTroopCounts) override {
const vector<PlayerBattleProgress> &playerBattleProgress) override {
if (!active_) return;
GameStatusResponse response;
@@ -568,10 +569,11 @@ auto EagleInterfaceImpl::SubscribeToGame(
std::string(currentGameState.begin(), currentGameState.end());
response.mutable_game_update_response()->set_current_round(currentRound);
for (const auto &[playerId, troopCount] : playerTroopCounts) {
for (const auto &[playerId, troopCount, isRetreating] : playerBattleProgress) {
auto *ptc = response.mutable_game_update_response()->add_player_troop_counts();
ptc->set_player_id(playerId);
ptc->set_troop_count(troopCount);
ptc->set_is_retreating(isRetreating);
}
ScopedShardokLatencyTrace trace(
@@ -80,6 +80,7 @@ message OnePlayerUpdateResponse {
message PlayerTroopCount {
int32 player_id = 1;
int32 troop_count = 2;
bool is_retreating = 3;
}
message GameUpdateResponse {
@@ -20,6 +20,7 @@ message Army {
int32 faction_id = 1;
repeated .net.eagle0.eagle.common.CombatUnit units = 2;
.google.protobuf.Int32Value flee_province_id = 3;
bool is_retreating = 4;
}
message MovingArmy {
@@ -29,4 +29,5 @@ message IncomingArmyView {
BattalionView battalion = 2;
}
repeated UnitDetails unit_details = 9;
bool is_retreating = 10;
}
@@ -34,4 +34,6 @@ message ShardokBattleView {
.google.protobuf.Int32Value winning_faction_id = 10; // set when battle ended and result is revealed
bool can_fight = 11; // true only for this viewer's highest fightable battle
bool can_observe = 12; // true if this viewer can currently observe this battle
bool defender_retreating = 13; // true once any defending unit has fled
bool attacker_retreating = 14; // true once any attacking unit has fled
}
@@ -69,7 +69,7 @@ object EndDefenseDecisionPhaseAction {
// Swap destination/origin, clear flee province, set arrival to next round
MovingArmy(
id = army.id,
army = army.army.copy(fleeProvinceId = None),
army = army.army.copy(fleeProvinceId = None, isRetreating = true),
arrivalRound = gameState.currentRoundId + 1,
destinationProvinceId = army.originProvinceId, // going back home
originProvinceId = province.id, // coming from paying province
@@ -82,7 +82,7 @@ case class PerformForcedTurnBackAction(gameState: GameState) extends ProtolessSe
provinceId = fleePid,
newIncomingArmies = Vector(
MovingArmy(
army = ia.army.copy(fleeProvinceId = None),
army = ia.army.copy(fleeProvinceId = None, isRetreating = true),
arrivalRound = gameState.currentRoundId + 1,
destinationProvinceId = fleePid,
originProvinceId = p.id,
@@ -910,7 +910,7 @@ case class ResolveBattleAction(
newIncomingArmies = Vector(
MovingArmy(
id = 0, // ID will be generated by ActionResultApplierImpl
army = fledArmy.copy(fleeProvinceId = None),
army = fledArmy.copy(fleeProvinceId = None, isRetreating = true),
arrivalRound = arrivalRound,
destinationProvinceId = destinationProvince,
originProvinceId = battleProvince,
@@ -40,7 +40,8 @@ case class WithdrawnArmiesReturnHomeAction(
destinationProvinceId = fleeProvinceId,
originProvinceId = originPid,
army = ma.army.copy(
fleeProvinceId = None
fleeProvinceId = None,
isRetreating = true
)
)
)
@@ -400,6 +400,7 @@ object ProvinceViewFilter {
arrivalRoundId = ma.arrivalRound,
food = ma.supplies.food,
gold = ma.supplies.gold,
isRetreating = ma.army.isRetreating,
unitDetails =
if showDetails then
ma.army.units.map { cu =>
@@ -24,14 +24,16 @@ object ArmyConverter {
ArmyProto(
factionId = army.factionId,
units = army.units.map(CombatUnitConverter.toProto),
fleeProvinceId = army.fleeProvinceId
fleeProvinceId = army.fleeProvinceId,
isRetreating = army.isRetreating
)
def fromProto(armyProto: ArmyProto): Army =
Army(
factionId = armyProto.factionId,
units = armyProto.units.map(CombatUnitConverter.fromProto).toVector,
fleeProvinceId = armyProto.fleeProvinceId
fleeProvinceId = armyProto.fleeProvinceId,
isRetreating = armyProto.isRetreating
)
def toProto(movingArmy: MovingArmy): MovingArmyProto =
@@ -20,7 +20,9 @@ object ShardokBattleViewConverter {
attackerTroopCount = view.attackerTroopCount,
winningFactionId = view.winningFactionId,
canFight = view.canFight,
canObserve = view.canObserve
canObserve = view.canObserve,
defenderRetreating = view.defenderRetreating,
attackerRetreating = view.attackerRetreating
)
private def toProtoPlayerInfo(info: ShardokBattlePlayerInfo): ShardokBattlePlayerInfoProto =
@@ -15,7 +15,8 @@ object IncomingArmyViewConverter {
arrivalRoundId = view.arrivalRoundId,
food = view.food,
gold = view.gold,
unitDetails = view.unitDetails.map(unitDetailsToProto)
unitDetails = view.unitDetails.map(unitDetailsToProto),
isRetreating = view.isRetreating
)
def fromProto(proto: IncomingArmyViewProto): IncomingArmyView =
@@ -28,7 +29,8 @@ object IncomingArmyViewConverter {
arrivalRoundId = proto.arrivalRoundId,
food = proto.food,
gold = proto.gold,
unitDetails = proto.unitDetails.map(unitDetailsFromProto).toVector
unitDetails = proto.unitDetails.map(unitDetailsFromProto).toVector,
isRetreating = proto.isRetreating
)
private def unitDetailsToProto(
@@ -5,7 +5,8 @@ import net.eagle0.eagle.{FactionId, MovingArmyId, ProvinceId, RoundId}
case class Army(
factionId: FactionId,
units: Vector[CombatUnit],
fleeProvinceId: Option[ProvinceId] = None
fleeProvinceId: Option[ProvinceId] = None,
isRetreating: Boolean = false
)
case class MovingArmy(
@@ -22,5 +22,7 @@ case class ShardokBattleView(
attackerTroopCount: Option[Int],
winningFactionId: Option[FactionId],
canFight: Boolean,
canObserve: Boolean
canObserve: Boolean,
defenderRetreating: Boolean = false,
attackerRetreating: Boolean = false
)
@@ -17,5 +17,6 @@ case class IncomingArmyView(
arrivalRoundId: RoundId,
food: Int = 0,
gold: Int = 0,
unitDetails: Vector[UnitDetails] = Vector.empty
unitDetails: Vector[UnitDetails] = Vector.empty,
isRetreating: Boolean = false
)
@@ -2,7 +2,12 @@ package net.eagle0.eagle.service.controller
import net.eagle0.eagle.FactionId
case class BattleProgressSnapshot(defenderTroops: Int, attackerTroops: Int) {
case class BattleProgressSnapshot(
defenderTroops: Int,
attackerTroops: Int,
defenderRetreating: Boolean = false,
attackerRetreating: Boolean = false
) {
def defenderRatio: Float = {
val total = defenderTroops + attackerTroops
if total == 0 then 0.5f else defenderTroops.toFloat / total
@@ -505,20 +505,29 @@ object GameController {
val currentRound = battleUpdate.currentRound
// Determine which player IDs are defenders from the battle's player list
val defenderPlayerIds = battleUpdate.battle.players.zipWithIndex
val defenderPlayerIds = battleUpdate.battle.players.zipWithIndex
.filter(_._1.isDefender)
.map(_._2)
.toSet
val defenderTroops = battleUpdate.playerTroopCounts
val defenderTroops = battleUpdate.playerTroopCounts
.filter(ptc => defenderPlayerIds.contains(ptc.playerId))
.map(_.troopCount)
.sum
val attackerTroops = battleUpdate.playerTroopCounts
val attackerTroops = battleUpdate.playerTroopCounts
.filterNot(ptc => defenderPlayerIds.contains(ptc.playerId))
.map(_.troopCount)
.sum
val defenderRetreating = battleUpdate.playerTroopCounts
.exists(ptc => defenderPlayerIds.contains(ptc.playerId) && ptc.isRetreating)
val attackerRetreating = battleUpdate.playerTroopCounts
.exists(ptc => !defenderPlayerIds.contains(ptc.playerId) && ptc.isRetreating)
val snapshot = BattleProgressSnapshot(defenderTroops, attackerTroops)
val snapshot = BattleProgressSnapshot(
defenderTroops,
attackerTroops,
defenderRetreating,
attackerRetreating
)
val tracker = current.getOrElse(
shardokGameId,
@@ -570,6 +579,8 @@ object GameController {
defenderRatio = snapshot.defenderRatio,
defenderTroopCount = Option.when(isAllied)(snapshot.defenderTroops),
attackerTroopCount = Option.when(isAllied)(snapshot.attackerTroops),
defenderRetreating = snapshot.defenderRetreating,
attackerRetreating = snapshot.attackerRetreating,
winningFactionId =
// Only reveal winner when the gated round has caught up to the end round
tracker.endedAtRound.filter(_ <= gatedRound).flatMap(_ => tracker.winningFactionId)
@@ -7,6 +7,7 @@ cc_test(
copts = TEST_COPTS,
linkstatic = True,
deps = [
"//src/main/cpp/net/eagle0/shardok/controller",
"//src/main/cpp/net/eagle0/shardok/library:engine",
"//src/test/cpp/net/eagle0/shardok/library:game_start_test_data",
"//src/test/cpp/net/eagle0/shardok/library:hex_map_test_data",
@@ -1,36 +1,13 @@
#include <gtest/gtest.h>
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/GameStart_test_data.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/HexMap_test_data.hpp"
#include "src/test/cpp/net/eagle0/shardok/library/ShardokEngineBasedTestData.hpp"
using net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT;
using net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
using net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
// Helper: extract troop counts from a game state using the same logic as
// ShardokGameController::GetUpdates().
static auto ExtractTroopCounts(const shardok::GameStateW &gsW)
-> std::vector<std::pair<int32_t, int32_t>> {
std::vector<std::pair<int32_t, int32_t>> counts;
const auto *gs = gsW.Get();
for (const auto *player : *gs->player_infos()) {
int32_t troopCount = 0;
for (const auto *unit : *gs->units()) {
if (unit->player_id() != player->player_id()) continue;
switch (unit->status()) {
case UnitStatus_NORMAL_UNIT:
case UnitStatus_RESERVE_UNIT:
case UnitStatus_NEVER_ENTERED_UNIT: troopCount += unit->battalion().size(); break;
default: break;
}
}
counts.emplace_back(player->player_id(), troopCount);
}
return counts;
}
using net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT;
using net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT;
TEST(BattleProgressFieldsTest, InitialState_HasCorrectRoundAndTroopCounts) {
// GameWithUnits creates: player 0 = attacker, player 1 = defender
@@ -50,17 +27,18 @@ TEST(BattleProgressFieldsTest, InitialState_HasCorrectRoundAndTroopCounts) {
// GameWithUnitTs sets current_round to 1
EXPECT_EQ(gs->current_round(), 1);
auto troopCounts = ExtractTroopCounts(gsW);
auto battleProgress = shardok::PlayerBattleProgressFrom(*gs);
// Should have entries for both players
ASSERT_EQ(troopCounts.size(), 2);
ASSERT_EQ(battleProgress.size(), 2);
// Find attacker (player 0) and defender (player 1) counts
int32_t attackerTroops = -1;
int32_t defenderTroops = -1;
for (const auto &[pid, count] : troopCounts) {
for (const auto &[pid, count, isRetreating] : battleProgress) {
if (pid == 0) attackerTroops = count;
if (pid == 1) defenderTroops = count;
EXPECT_FALSE(isRetreating);
}
// 2 attacker units x 500 = 1000
@@ -79,19 +57,40 @@ TEST(BattleProgressFieldsTest, ReservedSlotUnitsNotCounted) {
// GameWithUnitsAndReservedSlots adds RESERVED_SLOT units (player_id = -1)
auto engine = GameWithUnitsAndReservedSlots(BASIC_MAP, attackers, defenders, 5);
auto troopCounts = ExtractTroopCounts(engine->GetCurrentGameState());
auto battleProgress = shardok::PlayerBattleProgressFrom(*engine->GetCurrentGameState().Get());
// Only the two real players should appear (RESERVED_SLOT units have player_id -1
// and aren't associated with any player_info entry)
ASSERT_EQ(troopCounts.size(), 2);
ASSERT_EQ(battleProgress.size(), 2);
int32_t attackerTroops = -1;
int32_t defenderTroops = -1;
for (const auto &[pid, count] : troopCounts) {
for (const auto &[pid, count, isRetreating] : battleProgress) {
if (pid == 0) attackerTroops = count;
if (pid == 1) defenderTroops = count;
EXPECT_FALSE(isRetreating);
}
EXPECT_EQ(attackerTroops, 500);
EXPECT_EQ(defenderTroops, 500);
}
TEST(BattleProgressFieldsTest, FledUnitMarksPlayerAsRetreatingButRetreatedUnitDoesNot) {
auto fleeingAttacker = AddGenericUnit(0, 0, Coords(3, 3));
vector<Unit> attackers{fleeingAttacker};
auto retreatedDefender = AddGenericUnit(1, 0, Coords(2, 2));
vector<Unit> defenders{retreatedDefender};
auto engine = GameWithUnits(BASIC_MAP, attackers, defenders);
auto gameState = engine->GetCurrentGameState();
const_cast<Unit *>(gameState->mutable_units()->GetMutableObject(0))
->mutate_status(UnitStatus_FLED_UNIT);
const_cast<Unit *>(gameState->mutable_units()->GetMutableObject(1))
->mutate_status(UnitStatus_RETREATED_UNIT);
const auto progress = shardok::PlayerBattleProgressFrom(*gameState.Get());
ASSERT_EQ(progress.size(), 2);
EXPECT_TRUE(progress[0].isRetreating);
EXPECT_FALSE(progress[1].isRetreating);
}
@@ -153,6 +153,7 @@ class EndDefenseDecisionPhaseActionTest extends AnyFlatSpec with Matchers {
province7Changes.newIncomingArmies.head.originProvinceId shouldBe defendingProvinceId
province7Changes.newIncomingArmies.head.destinationProvinceId shouldBe 7
province7Changes.newIncomingArmies.head.arrivalRound shouldBe 20
province7Changes.newIncomingArmies.head.army.isRetreating shouldBe true
}
// Province 27 should have incoming army returning
@@ -147,7 +147,8 @@ class PerformForcedTurnBackActionTest extends AnyFlatSpec with Matchers with Bef
army = Army(
factionId = 54,
fleeProvinceId = None,
units = Vector(CombatUnit(factionId = 54, heroId = 5, battalionId = None))
units = Vector(CombatUnit(factionId = 54, heroId = 5, battalionId = None)),
isRetreating = true
),
arrivalRound = 28,
originProvinceId = destinationPid,
@@ -2008,5 +2008,6 @@ class ResolveBattleActionTest extends AnyFlatSpec with MockFactory with Matchers
(1, Some(88))
)
incomingArmy.army.fleeProvinceId shouldBe None // Consumed by fleeing
incomingArmy.army.isRetreating shouldBe true
}
}
@@ -87,7 +87,8 @@ class WithdrawnArmiesReturnHomeActionTest extends AnyFlatSpec with Matchers {
army = Army(
fleeProvinceId = None,
factionId = withdrawingFactionId,
units = Vector.empty
units = Vector.empty,
isRetreating = true
),
arrivalRound = currentRoundId + 1,
destinationProvinceId = 5,
@@ -108,7 +109,8 @@ class WithdrawnArmiesReturnHomeActionTest extends AnyFlatSpec with Matchers {
army = Army(
fleeProvinceId = None,
factionId = withdrawingFactionId,
units = Vector.empty
units = Vector.empty,
isRetreating = true
),
arrivalRound = currentRoundId + 1,
destinationProvinceId = 6,
@@ -180,7 +182,8 @@ class WithdrawnArmiesReturnHomeActionTest extends AnyFlatSpec with Matchers {
army = Army(
fleeProvinceId = None,
factionId = withdrawingFactionId,
units = Vector.empty
units = Vector.empty,
isRetreating = true
),
arrivalRound = currentRoundId + 1,
destinationProvinceId = 5,
@@ -22,6 +22,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
arrivalRoundId = 15: RoundId,
food = 100,
gold = 200,
isRetreating = true,
unitDetails = Vector(
UnitDetails(
heroId = 50: HeroId,
@@ -54,6 +55,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
food,
gold,
unitDetails,
isRetreating,
_ /* unknownFields */
) =>
factionId shouldBe 1
@@ -65,6 +67,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
food shouldBe 100
gold shouldBe 200
unitDetails.size shouldBe 2
isRetreating shouldBe true
// Verify unit details conversion
unitDetails.head match {
@@ -85,6 +88,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
arrivalRoundId = 25,
food = 300,
gold = 400,
isRetreating = true,
unitDetails = Seq(
IncomingArmyViewProto.UnitDetails(
heroId = 60,
@@ -115,7 +119,8 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
arrivalRoundId,
food,
gold,
unitDetails
unitDetails,
isRetreating
) =>
factionId shouldBe (2: FactionId)
heroCount shouldBe 5
@@ -126,6 +131,7 @@ class IncomingArmyViewConverterTest extends AnyFlatSpec with Matchers {
food shouldBe 300
gold shouldBe 400
unitDetails.size shouldBe 1
isRetreating shouldBe true
// Verify unit details conversion
unitDetails.head match {
@@ -122,6 +122,27 @@ class BattleProgressTest extends AnyFlatSpec with Matchers {
tracker.latestSnapshot.attackerTroops shouldBe 500
}
it should "aggregate retreating status by battle side" in {
val update = makeBattleUpdate(
currentRound = 1,
playerTroopCounts = Seq(
PlayerTroopCount(playerId = 0, troopCount = 500),
PlayerTroopCount(playerId = 1, troopCount = 300),
PlayerTroopCount(playerId = 2, troopCount = 200, isRetreating = true)
),
battle = threePlayerBattle
)
val result = GameController.updatedBattleProgress(Map.empty, "test-battle-1", update)
result("test-battle-1").latestSnapshot shouldBe BattleProgressSnapshot(
defenderTroops = 500,
attackerTroops = 500,
defenderRetreating = false,
attackerRetreating = true
)
}
it should "default to zero when defender has no troop count entry" in {
val update = makeBattleUpdate(
currentRound = 1,