implement graceful handling of API errors

This commit is contained in:
Rokas Puzonas 2024-08-29 22:39:35 +03:00
parent 805e2dcabc
commit 8cd5fb44c8
6 changed files with 574 additions and 199 deletions

View File

@ -11,7 +11,9 @@ const assert = std.debug.assert;
const SkillStats = @import("./skill_stats.zig");
const CombatStats = @import("./combat_stats.zig");
const Equipment = @import("./equipment.zig");
const Inventory = @import("./inventory.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
const Inventory = BoundedSlotsArray(20);
const Character = @This();
@ -94,11 +96,7 @@ pub fn deinit(self: *Character) void {
}
pub fn getItemCount(self: *const Character) u64 {
var count: u64 = 0;
for (self.inventory.slots) |slot| {
count += slot.quantity;
}
return count;
return self.inventory.totalQuantity();
}
pub fn format(

View File

@ -1,96 +0,0 @@
const std = @import("std");
const json_utils = @import("json_utils.zig");
const Server = @import("./server.zig");
const ItemId = Server.ItemId;
const assert = std.debug.assert;
const json = std.json;
const Inventory = @This();
const slot_count = 20;
pub const Slot = struct {
id: ?ItemId,
quantity: u64,
fn parse(api: *Server, slot_obj: json.ObjectMap) !Slot {
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
if (quantity < 0) return error.InvalidQuantity;
return Slot{
.id = try api.getItemIdJson(slot_obj, "code"),
.quantity = @intCast(quantity),
};
}
};
slots: [slot_count]Slot,
pub fn parse(api: *Server, slots_array: json.Array) !Inventory {
assert(slots_array.items.len == Inventory.slot_count);
var inventory: Inventory = undefined;
for (0.., slots_array.items) |i, slot_value| {
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
inventory.slots[i] = try Slot.parse(api, slot_obj);
}
return inventory;
}
fn findSlot(self: *Inventory, id: ItemId) ?*Slot {
for (&self.slots) |*slot| {
if (slot.id == id) {
return slot;
}
}
return null;
}
pub fn removeItem(self: *Inventory, id: ItemId, quantity: u64) void {
const slot = self.findSlot(id) orelse unreachable;
assert(slot.quantity >= quantity);
slot.quantity -= quantity;
if (slot.quantity == 0) {
slot.id = null;
}
}
pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void {
if (self.findSlot(id)) |slot| {
slot.quantity += quantity;
} else {
var empty_slot: ?*Slot = null;
for (&self.slots) |*slot| {
if (slot.id == null) {
empty_slot = slot;
}
}
assert(empty_slot != null);
empty_slot.?.id = id;
empty_slot.?.quantity = quantity;
}
}
pub fn addItems(self: *Inventory, items: []const Server.ItemIdQuantity) void {
for (items) |item| {
self.addItem(item.id, item.quantity);
}
}
pub fn removeItems(self: *Inventory, items: []const Server.ItemIdQuantity) void {
for (items) |item| {
self.removeItem(item.id, item.quantity);
}
}
pub fn getItem(self: *Inventory, id: ItemId) u64 {
if (self.findSlot(id)) |slot| {
return slot.quantity;
}
return 0;
}

View File

@ -8,6 +8,8 @@ pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const json_utils = @import("json_utils.zig");
pub const Character = @import("character.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
pub const Slot = @import("./slot.zig");
// Specification: https://api.artifactsmmo.com/docs
@ -252,16 +254,12 @@ pub const Cooldown = struct {
pub const FightResult = struct {
const Details = struct {
const DroppedItem = struct {
id: ItemId,
quantity: i64
};
const Result = enum { win, lose };
const Drops = BoundedSlotsArray(8);
xp: i64,
gold: i64,
drops: std.BoundedArray(DroppedItem, 8),
drops: Drops,
result: Result,
fn parse(api: *Server, obj: json.ObjectMap) !Details {
@ -271,24 +269,16 @@ pub const FightResult = struct {
result_enum = .win;
} else if (std.mem.eql(u8, result, "win")) {
result_enum = .lose;
} else {
return error.InvalidProperty;
}
var drops = std.BoundedArray(DroppedItem, 8).init(0) catch unreachable;
const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
for (drops_obj.items) |drop_value| {
const drop_obj = json_utils.asObject(drop_value) orelse return error.MissingProperty;
const code = try json_utils.getStringRequired(drop_obj, "code");
try drops.append(DroppedItem{
.id = try api.getItemId(code),
.quantity = try json_utils.getIntegerRequired(drop_obj, "quantity")
});
}
return Details{
.xp = try json_utils.getIntegerRequired(obj, "xp"),
.gold = try json_utils.getIntegerRequired(obj, "gold"),
.drops = drops,
.drops = try Drops.parse(api, drops_obj),
.result = result_enum,
};
}
@ -322,6 +312,7 @@ pub const FightResult = struct {
}
};
// TODO: Replace this with ItemSlot struct
pub const ItemIdQuantity = struct {
id: ItemId,
quantity: u64,
@ -338,29 +329,18 @@ pub const ItemIdQuantity = struct {
}
};
const BoundedItems = std.BoundedArray(ItemIdQuantity, 8);
fn parseSimpleItemList(api: *Server, array: json.Array) !BoundedItems {
var items = BoundedItems.init(0) catch unreachable;
for (array.items) |item_value| {
const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty;
try items.append(try ItemIdQuantity.parse(api, item_obj));
}
return items;
}
pub const SkillResultDetails = struct {
const Items = BoundedSlotsArray(8);
xp: i64,
items: BoundedItems,
items: Items,
fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails {
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return SkillResultDetails{
.xp = try json_utils.getIntegerRequired(obj, "xp"),
.items = try parseSimpleItemList(api, items),
.items = try Items.parse(api, items),
};
}
};
@ -640,10 +620,12 @@ pub const MapResult = struct {
pub const Item = struct {
pub const Recipe = struct {
const Items = BoundedSlotsArray(8);
skill: Skill,
level: u64,
quantity: u64,
items: BoundedItems,
items: Items,
pub fn parse(api: *Server, obj: json.ObjectMap) !Recipe {
const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty;
@ -659,7 +641,7 @@ pub const Item = struct {
.skill = Skill.parse(skill) orelse return error.InvalidSkill,
.level = @intCast(level),
.quantity = @intCast(quantity),
.items = try parseSimpleItemList(api, items)
.items = try Items.parse(api, items)
};
}
};
@ -1094,6 +1076,15 @@ pub fn findCharacter(self: *const Server, name: []const u8) ?Character {
return null;
}
pub fn findCharacterPtr(self: *Server, name: []const u8) ?*Character {
if (self.findCharacterIndex(name)) |index| {
return &self.characters.items[index];
}
return null;
}
pub fn findItem(self: *const Server, name: []const u8) ?Item {
return self.items.get(name);
}

23
src/api/slot.zig Normal file
View File

@ -0,0 +1,23 @@
const std = @import("std");
const json_utils = @import("json_utils.zig");
const Server = @import("./server.zig");
const ItemId = Server.ItemId;
const json = std.json;
const ItemSlot = @This();
id: ItemId,
quantity: u64,
pub fn parse(api: *Server, slot_obj: json.ObjectMap) !?ItemSlot {
const code = try json_utils.getStringRequired(slot_obj, "code");
if (code.len == 0) return null;
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
if (quantity < 0) return error.InvalidQuantity;
return ItemSlot{
.id = try api.getItemId(code),
.quantity = @intCast(quantity),
};
}

115
src/api/slot_array.zig Normal file
View File

@ -0,0 +1,115 @@
const std = @import("std");
const json_utils = @import("json_utils.zig");
const Server = @import("./server.zig");
const ItemId = Server.ItemId;
const assert = std.debug.assert;
const json = std.json;
const Slot = @import("./slot.zig");
pub fn BoundedSlotsArray(comptime slot_count: u32) type {
const Slots = std.BoundedArray(Slot, slot_count);
return struct {
slots: Slots,
fn init() @This() {
return @This(){
.slots = Slots.init(0) catch unreachable
};
}
pub fn parse(api: *Server, slots_array: json.Array) !@This() {
var slots = Slots.init(0) catch unreachable;
for (slots_array.items) |slot_value| {
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
if (try Slot.parse(api, slot_obj)) |slot| {
try slots.append(slot);
}
}
return @This(){ .slots = slots };
}
fn findSlotIndex(self: *const @This(), id: ItemId) ?usize {
for (0.., self.slots.slice()) |i, *slot| {
if (slot.id == id) {
return i;
}
}
return null;
}
fn findSlot(self: *@This(), id: ItemId) ?*Slot {
if (self.findSlotIndex(id)) |index| {
return &self.slots.buffer[index];
}
return null;
}
pub fn remove(self: *@This(), id: ItemId, quantity: u64) void {
const slot_index = self.findSlotIndex(id) orelse unreachable;
const slot = self.slots.get(slot_index);
assert(slot.quantity >= quantity);
slot.quantity -= quantity;
if (slot.quantity == 0) {
self.slots.swapRemove(slot_index);
}
}
pub fn add(self: *@This(), id: ItemId, quantity: u64) void {
if (self.findSlot(id)) |slot| {
slot.quantity += quantity;
} else {
var empty_slot: ?*Slot = null;
for (&self.slots) |*slot| {
if (slot.id == null) {
empty_slot = slot;
}
}
assert(empty_slot != null);
empty_slot.?.id = id;
empty_slot.?.quantity = quantity;
}
}
pub fn addSlice(self: *@This(), items: []const Server.ItemIdQuantity) void {
for (items) |item| {
self.add(item.id, item.quantity);
}
}
pub fn removeSlice(self: *@This(), items: []const Server.ItemIdQuantity) void {
for (items) |item| {
self.remove(item.id, item.quantity);
}
}
pub fn getQuantity(self: *const @This(), id: ItemId) u64 {
if (self.findSlotIndex(id)) |index| {
return self.slots.get(index).quantity;
}
return 0;
}
pub fn totalQuantity(self: *const @This()) u64 {
var count: u64 = 0;
for (self.slots.constSlice()) |slot| {
count += slot.quantity;
}
return count;
}
pub fn slice(self: *@This()) []Slot {
return self.slots.slice();
}
};
}

View File

@ -17,6 +17,89 @@ const QueuedAction = union(enum) {
craft_item: Server.ItemIdQuantity,
};
const QueuedActionResult = union(enum) {
move: Server.MoveError!Server.MoveResult,
fight: Server.FightError!Server.FightResult,
gather: Server.GatherError!Server.GatherResult,
deposit_gold: Server.BankDepositGoldError!Server.GoldTransactionResult,
deposit_item: Server.BankDepositItemError!Server.ItemTransactionResult,
withdraw_item: Server.BankWithdrawItemError!Server.ItemTransactionResult,
craft_item: Server.CraftError!Server.CraftResult,
const Tag = @typeInfo(QueuedActionResult).Union.tag_type.?;
fn fieldType(comptime kind: Tag) type {
const field_type = std.meta.fields(QueuedActionResult)[@intFromEnum(kind)].type;
return @typeInfo(field_type).ErrorUnion.payload;
}
pub fn get(self: QueuedActionResult, comptime kind: Tag) ?fieldType(kind) {
return switch (self) {
kind => |v| v catch null,
else => null
};
}
fn getMove() Server.MoveResult {
}
};
comptime {
assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len);
}
fn performAction(api: *Server, name: []const u8, action: QueuedAction) !QueuedActionResult {
const log = std.log.default;
switch (action) {
.fight => {
log.debug("[{s}] attack", .{name});
return .{
.fight = api.actionFight(name)
};
},
.move => |pos| {
log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y});
return .{
.move = api.actionMove(name, pos.x, pos.y)
};
},
.deposit_gold => |quantity| {
log.debug("[{s}] deposit {} gold", .{name, quantity});
return .{
.deposit_gold = api.actionBankDepositGold(name, quantity)
};
},
.deposit_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity});
return .{
.deposit_item = api.actionBankDepositItem(name, code, item.quantity)
};
},
.withdraw_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity});
return .{
.withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity)
};
},
.gather => {
log.debug("[{s}] gather", .{name});
return .{
.gather = api.actionGather(name)
};
},
.craft_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity});
return .{
.craft_item = api.actionCraft(name, code, item.quantity)
};
}
}
}
const CharacterBrain = struct {
name: []const u8,
routine: union (enum) {
@ -54,45 +137,285 @@ const CharacterBrain = struct {
}
fn performNextAction(self: *CharacterBrain, api: *Server) !void {
const log = std.log.default;
assert(self.action_queue.items.len > 0);
const log = std.log.default;
const APIError = Server.APIError;
switch (self.action_queue.items[0]) {
.fight => {
log.debug("{s} attacks", .{self.name});
_ = try api.actionFight(self.name);
const retry_delay = 0.5; // 500ms
var character = api.findCharacterPtr(self.name).?;
const action_result = try performAction(api, self.name, self.action_queue.items[0]);
switch (action_result) {
.fight => |result| {
const FightError = Server.FightError;
_ = result catch |err| switch (err) {
FightError.CharacterInCooldown,
FightError.CharacterIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry fighting", .{self.name});
return;
},
FightError.CharacterIsFull,
FightError.MonsterNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
FightError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.move => |pos| {
log.debug("move {s} to ({}, {})", .{self.name, pos.x, pos.y});
_ = try api.actionMove(self.name, pos.x, pos.y);
.move => |result| {
const MoveError = Server.MoveError;
_ = result catch |err| switch (err) {
MoveError.CharacterIsBusy,
MoveError.CharacterInCooldown => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry moving", .{self.name});
return;
},
MoveError.CharacterAtDestination => {
// Not great, but I guess the goal achieved? The character is at the desired location.
log.warn("[{s}] tried to move, but already at destination", .{self.name});
},
MoveError.MapNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
MoveError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.deposit_gold => |quantity| {
log.debug("deposit {} gold from {s}", .{quantity, self.name});
_ = try api.actionBankDepositGold(self.name, quantity);
.deposit_gold => |result| {
const BankDepositGoldError = Server.BankDepositGoldError;
_ = result catch |err| switch (err) {
BankDepositGoldError.BankIsBusy,
BankDepositGoldError.CharacterIsBusy,
BankDepositGoldError.CharacterInCooldown => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry depositing gold", .{self.name});
return;
},
BankDepositGoldError.NotEnoughGold,
BankDepositGoldError.BankNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
BankDepositGoldError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.deposit_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("deposit {s} (x{}) from {s}", .{code, item.quantity, self.name});
_ = try api.actionBankDepositItem(self.name, code, item.quantity);
.deposit_item => |result| {
const BankDepositItemError = Server.BankDepositItemError;
_ = result catch |err| switch (err) {
BankDepositItemError.BankIsBusy,
BankDepositItemError.CharacterIsBusy,
BankDepositItemError.CharacterInCooldown => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry depositing item", .{self.name});
return;
},
BankDepositItemError.ItemNotFound,
BankDepositItemError.NotEnoughItems,
BankDepositItemError.BankNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
BankDepositItemError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.withdraw_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("withdraw {s} (x{}) from {s}", .{code, item.quantity, self.name});
_ = try api.actionBankWithdrawItem(self.name, code, item.quantity);
.withdraw_item => |result| {
const BankWithdrawItemError = Server.BankWithdrawItemError;
_ = result catch |err| switch (err) {
BankWithdrawItemError.CharacterIsBusy,
BankWithdrawItemError.CharacterInCooldown,
BankWithdrawItemError.BankIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry withdrawing item", .{self.name});
return;
},
BankWithdrawItemError.ItemNotFound,
BankWithdrawItemError.NotEnoughItems,
BankWithdrawItemError.CharacterIsFull,
BankWithdrawItemError.BankNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
BankWithdrawItemError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.gather => {
log.debug("{s} gathers", .{self.name});
_ = try api.actionGather(self.name);
.gather => |result| {
const GatherError = Server.GatherError;
_ = result catch |err| switch (err) {
GatherError.CharacterInCooldown,
GatherError.CharacterIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry withdrawing item", .{self.name});
return;
},
GatherError.NotEnoughSkill,
GatherError.CharacterIsFull,
GatherError.ResourceNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
GatherError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
},
.craft_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
log.debug("craft {s} (x{}) from {s}", .{code, item.quantity, self.name});
_ = try api.actionCraft(self.name, code, item.quantity);
.craft_item => |result| {
const CraftError = Server.CraftError;
_ = result catch |err| switch (err) {
CraftError.CharacterInCooldown,
CraftError.CharacterIsBusy => {
// A bit too eager, retry action
character.cooldown_expiration = currentTime() + retry_delay;
log.warn("[{s}] retry withdrawing item", .{self.name});
return;
},
CraftError.RecipeNotFound,
CraftError.NotEnoughItems,
CraftError.NotEnoughSkill,
CraftError.CharacterIsFull,
CraftError.WorkshopNotFound => {
// Re-evaluate what the character should do, something is not right.
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
CraftError.CharacterNotFound,
APIError.ServerUnavailable,
APIError.RequestFailed,
APIError.ParseFailed,
APIError.OutOfMemory => {
// Welp... Abondon ship. Bail. Bail
return err;
}
};
}
}
_ = self.action_queue.orderedRemove(0);
self.onActionCompleted(action_result);
}
fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void {
switch (self.routine) {
.idle => {},
.fight => |*args| {
if (result.get(.fight)) |r| {
const fight_result: Server.FightResult = r;
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.target.id);
}
},
.gather => |*args| {
if (result.get(.gather)) |r| {
const gather_resutl: Server.GatherResult = r;
const items = gather_resutl.details.items;
args.progress += items.getQuantity(args.target.id);
}
},
.craft => |*args| {
if (result.get(.craft_item)) |r| {
const craft_result: Server.CraftResult = r;
const items = craft_result.details.items;
args.progress += items.getQuantity(args.target.id);
}
}
}
}
fn isRoutineFinished(self: *CharacterBrain) bool {
return switch (self.routine) {
.idle => false,
.fight => |args| {
return args.progress >= args.target.quantity;
},
.gather => |args| {
return args.progress >= args.target.quantity;
},
.craft => |args| {
return args.progress >= args.target.quantity;
}
};
}
};
@ -185,7 +508,7 @@ fn moveIfNeeded(api: *Server, brain: *CharacterBrain, pos: Position) !bool {
}
fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool {
const character = api.findCharacter(brain.name).?;
var character = api.findCharacter(brain.name).?;
const action_queue = &brain.action_queue;
// Deposit items and gold to bank if full
@ -197,14 +520,10 @@ fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool {
try action_queue.append(.{ .move = bank_position });
}
for (character.inventory.slots) |slot| {
if (slot.quantity == 0) continue;
if (slot.id) |item_id| {
try action_queue.append(.{
.deposit_item = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
});
}
for (character.inventory.slice()) |slot| {
try action_queue.append(.{
.deposit_item = .{ .id = slot.id, .quantity = slot.quantity }
});
}
return true;
@ -249,12 +568,12 @@ fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Positi
try brain.action_queue.append(.{ .gather = {} });
}
fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.ItemIdQuantity) !bool {
fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.Slot) !bool {
var character = api.findCharacter(brain.name).?;
var has_all_items = true;
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getItem(item_quantity.id);
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
@ -267,7 +586,7 @@ fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.
}
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getItem(item_quantity.id);
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try brain.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
@ -282,7 +601,7 @@ fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.
fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !bool {
var character = api.findCharacter(brain.name).?;
const inventory_quantity = character.inventory.getItem(id);
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
return false;
}
@ -319,7 +638,7 @@ fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity:
fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !void {
var character = api.findCharacter(brain.name).?;
const inventory_quantity = character.inventory.getItem(id);
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
if (try depositItemsToBank(api, brain)) {
return;
@ -340,7 +659,7 @@ fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantit
needed_item.quantity *= quantity;
}
if (try withdrawFromBank(api, brain, needed_items.constSlice())) {
if (try withdrawFromBank(api, brain, needed_items.slice())) {
return;
}
@ -375,64 +694,89 @@ pub fn main() !void {
goal_manager.characters.items[0].routine = .{
.fight = .{
.at = Position.init(0, 1),
.target = undefined
.target = .{
.id = try api.getItemId("egg"),
.quantity = 1
}
},
};
goal_manager.characters.items[1].routine = .{
.gather = .{
.at = Position.init(-1, 0),
.target = undefined
.target = .{
.id = try api.getItemId("ash_wood"),
.quantity = 3
}
}
};
goal_manager.characters.items[2].routine = .{
.gather = .{
.at = Position.init(2, 0),
.target = undefined
.target = .{
.id = try api.getItemId("copper_ore"),
.quantity = 3
}
}
};
goal_manager.characters.items[3].routine = .{
.gather = .{
.at = Position.init(4, 2),
.target = undefined
.target = .{
.id = try api.getItemId("gudgeon"),
.quantity = 3
}
}
};
goal_manager.characters.items[4].routine = .{
.fight = .{
.at = Position.init(0, 1),
.target = undefined
.target = .{
.id = try api.getItemId("raw_chicken"),
.quantity = 1
}
},
};
// goal_manager.characters.items[2].routine = .{
// .craft = .{
// .target = .{
// .quantity = 3,
// .id = try api.getItemId("copper"),
// }
// }
// };
const APIError = Server.APIError;
std.log.info("Starting main loop", .{});
while (true) {
try goal_manager.runNextAction();
goal_manager.runNextAction() catch |err| switch (err) {
APIError.ServerUnavailable => {
// If the server is down, wait for a moment and try again.
std.time.sleep(std.time.ns_per_min * 5);
continue;
},
for (goal_manager.characters.items) |*character| {
if (character.action_queue.items.len > 0) continue;
// TODO: Log all other error to a file or something. So it could be review later on.
else => return err
};
switch (character.routine) {
.idle => {},
for (goal_manager.characters.items) |*brain| {
if (brain.action_queue.items.len > 0) continue;
if (brain.isRoutineFinished()) {
if (!try depositItemsToBank(&api, brain)) {
brain.routine = .idle;
}
continue;
}
switch (brain.routine) {
.idle => {
std.log.debug("[{s}] idle", .{brain.name});
},
.fight => |args| {
try fightRoutine(&api, character, args.at);
try fightRoutine(&api, brain, args.at);
},
.gather => |args| {
try gatherRoutine(&api, character, args.at);
try gatherRoutine(&api, brain, args.at);
},
.craft => |args| {
try craftRoutine(&api, character, args.target.id, args.target.quantity);
try craftRoutine(&api, brain, args.target.id, args.target.quantity);
}
}
}