refactor main loop

This commit is contained in:
Rokas Puzonas 2024-08-28 01:20:28 +03:00
parent b33754efb8
commit 805e2dcabc
14 changed files with 892 additions and 779 deletions

View File

@ -11,8 +11,8 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
exe.linkLibC();
exe.addIncludePath(b.path("src"));
exe.addCSourceFile(.{ .file = b.path("src/timegm.c") });
exe.addIncludePath(b.path("src/date_time"));
exe.addCSourceFile(.{ .file = b.path("src/date_time/timegm.c") });
b.installArtifact(exe);

118
src/api/character.zig Normal file
View File

@ -0,0 +1,118 @@
const std = @import("std");
const json_utils = @import("json_utils.zig");
const Server = @import("./server.zig");
const Position = @import("./position.zig");
const parseDateTime = Server.parseDateTime;
const ItemId = Server.ItemId;
const Allocator = std.mem.Allocator;
const json = std.json;
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 Character = @This();
allocator: Allocator,
name: []u8,
skin: []u8,
account: ?[]u8,
gold: i64,
hp: i64,
haste: i64,
position: Position,
cooldown_expiration: f64,
combat: SkillStats,
mining: SkillStats,
woodcutting: SkillStats,
fishing: SkillStats,
weaponcrafting: SkillStats,
gearcrafting: SkillStats,
jewelrycrafting: SkillStats,
cooking: SkillStats,
water: CombatStats,
fire: CombatStats,
earth: CombatStats,
air: CombatStats,
equipment: Equipment,
inventory_max_items: i64,
inventory: Inventory,
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character {
const inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty;
const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") orelse return error.MissingProperty;
const x = try json_utils.getIntegerRequired(obj, "x");
const y = try json_utils.getIntegerRequired(obj, "y");
const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty;
assert(name.len > 0);
return Character{
.allocator = allocator,
.account = try json_utils.dupeString(allocator, obj, "account"),
.name = name,
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
.gold = try json_utils.getIntegerRequired(obj, "gold"),
.hp = try json_utils.getIntegerRequired(obj, "hp"),
.haste = try json_utils.getIntegerRequired(obj, "haste"),
.position = Position.init(x, y),
.cooldown_expiration = parseDateTime(cooldown_expiration) orelse return error.InvalidDateTime,
.combat = try SkillStats.parse(obj, "level", "xp", "max_xp"),
.mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"),
.woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"),
.fishing = try SkillStats.parse(obj, "fishing_level", "fishing_xp", "fishing_max_xp"),
.weaponcrafting = try SkillStats.parse(obj, "weaponcrafting_level", "weaponcrafting_xp", "weaponcrafting_max_xp"),
.gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"),
.jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"),
.cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"),
.water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"),
.fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"),
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
.equipment = try Equipment.parse(api, obj),
.inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
.inventory = try Inventory.parse(api, inventory)
};
}
pub fn deinit(self: *Character) void {
if (self.account) |str| self.allocator.free(str);
self.allocator.free(self.name);
self.allocator.free(self.skin);
}
pub fn getItemCount(self: *const Character) u64 {
var count: u64 = 0;
for (self.inventory.slots) |slot| {
count += slot.quantity;
}
return count;
}
pub fn format(
self: Character,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{s}{{ .name = \"{s}\", .position = {} ... }}", .{
@typeName(Character),
self.name,
self.position
});
}

17
src/api/combat_stats.zig Normal file
View File

@ -0,0 +1,17 @@
const std = @import("std");
const json_utils = @import("json_utils.zig");
const json = std.json;
const CombatStats = @This();
attack: i64,
damage: i64,
resistance: i64,
pub fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
return CombatStats{
.attack = try json_utils.getIntegerRequired(object, attack),
.damage = try json_utils.getIntegerRequired(object, damage),
.resistance = try json_utils.getIntegerRequired(object, resistance),
};
}

90
src/api/equipment.zig Normal file
View File

@ -0,0 +1,90 @@
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 Equipment = @This();
pub const Consumable = struct {
id: ?ItemId,
quantity: i64,
fn parse(api: *Server, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
return Consumable{
.id = try api.getItemIdJson(obj, name),
.quantity = try json_utils.getIntegerRequired(obj, quantity),
};
}
};
pub const Slot = enum {
weapon,
shield,
helmet,
body_armor,
leg_armor,
boots,
ring1,
ring2,
amulet,
artifact1,
artifact2,
consumable1,
consumable2,
fn name(self: Slot) []const u8 {
return switch (self) {
.weapon => "weapon",
.shield => "shield",
.helmet => "helmet",
.body_armor => "body_armor",
.leg_armor => "leg_armor",
.boots => "boots",
.ring1 => "ring1",
.ring2 => "ring2",
.amulet => "amulet",
.artifact1 => "artifact1",
.artifact2 => "artifact2",
.consumable1 => "consumable1",
.consumable2 => "consumable2",
};
}
};
weapon: ?ItemId,
shield: ?ItemId,
helmet: ?ItemId,
body_armor: ?ItemId,
leg_armor: ?ItemId,
boots: ?ItemId,
ring1: ?ItemId,
ring2: ?ItemId,
amulet: ?ItemId,
artifact1: ?ItemId,
artifact2: ?ItemId,
artifact3: ?ItemId,
consumable1: Consumable,
consumable2: Consumable,
pub fn parse(api: *Server, obj: json.ObjectMap) !Equipment {
return Equipment{
.weapon = try api.getItemIdJson(obj, "weapon_slot"),
.shield = try api.getItemIdJson(obj, "shield_slot"),
.helmet = try api.getItemIdJson(obj, "helmet_slot"),
.body_armor = try api.getItemIdJson(obj, "body_armor_slot"),
.leg_armor = try api.getItemIdJson(obj, "leg_armor_slot"),
.boots = try api.getItemIdJson(obj, "boots_slot"),
.ring1 = try api.getItemIdJson(obj, "ring1_slot"),
.ring2 = try api.getItemIdJson(obj, "ring2_slot"),
.amulet = try api.getItemIdJson(obj, "amulet_slot"),
.artifact1 = try api.getItemIdJson(obj, "artifact1_slot"),
.artifact2 = try api.getItemIdJson(obj, "artifact2_slot"),
.artifact3 = try api.getItemIdJson(obj, "artifact3_slot"),
.consumable1 = try Consumable.parse(api, obj, "consumable1_slot", "consumable1_slot_quantity"),
.consumable2 = try Consumable.parse(api, obj, "consumable2_slot", "consumable2_slot_quantity"),
};
}

96
src/api/inventory.zig Normal file
View File

@ -0,0 +1,96 @@
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;
}

15
src/api/position.zig Normal file
View File

@ -0,0 +1,15 @@
const Position = @This();
x: i64,
y: i64,
pub fn init(x: i64, y: i64) Position {
return Position{
.x = x,
.y = y
};
}
pub fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
}

View File

@ -3,13 +3,17 @@ const assert = std.debug.assert;
const json = std.json;
const Allocator = std.mem.Allocator;
// TODO: Maybe it would be good to move date time parsing to separate module
pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const json_utils = @import("json_utils.zig");
pub const Character = @import("character.zig");
// Specification: https://api.artifactsmmo.com/docs
const ArtifactsAPI = @This();
const Server = @This();
const log = std.log.scoped(.api);
pub const ItemId = u32;
allocator: Allocator,
@ -21,6 +25,8 @@ server_uri: std.Uri,
token: ?[]u8 = null,
item_codes: std.ArrayList([]u8),
characters: std.ArrayList(Character),
items: std.StringHashMap(Item),
// ------------------------- API errors ------------------------
@ -126,39 +132,7 @@ pub const EquipError = APIError || error {
// ------------------------- API result structs ------------------------
pub const EquipmentSlot = enum {
weapon,
shield,
helmet,
body_armor,
leg_armor,
boots,
ring1,
ring2,
amulet,
artifact1,
artifact2,
consumable1,
consumable2,
fn name(self: EquipmentSlot) []const u8 {
return switch (self) {
.weapon => "weapon",
.shield => "shield",
.helmet => "helmet",
.body_armor => "body_armor",
.leg_armor => "leg_armor",
.boots => "boots",
.ring1 => "ring1",
.ring2 => "ring2",
.amulet => "amulet",
.artifact1 => "artifact1",
.artifact2 => "artifact2",
.consumable1 => "consumable1",
.consumable2 => "consumable2",
};
}
};
pub const EquipmentSlot = @import("./equipment.zig").Slot;
pub const Skill = enum {
weaponcrafting,
@ -198,7 +172,7 @@ const ServerStatus = struct {
version: []const u8,
characters_online: i64,
pub fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
pub fn parse(api: *Server, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
_ = api;
return ServerStatus{
@ -215,55 +189,6 @@ const ServerStatus = struct {
}
};
pub fn parseDateTime(datetime: []const u8) ?f64 {
const time_h = @cImport({
@cDefine("_XOPEN_SOURCE", "700");
@cInclude("stddef.h");
@cInclude("stdio.h");
@cInclude("time.h");
@cInclude("timegm.h");
});
var buffer: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
var allocator = fba.allocator();
const datetime_z = allocator.dupeZ(u8, datetime) catch return null;
var tm: time_h.tm = .{};
const s = time_h.strptime(datetime_z, "%Y-%m-%dT%H:%M:%S.", &tm);
if (s == null) {
return null;
}
const s_len = std.mem.len(s);
if (s[s_len-1] != 'Z') {
return null;
}
const milliseconds_str = s[0..(s_len-1)];
var milliseconds: f64 = @floatFromInt(std.fmt.parseUnsigned(u32, milliseconds_str, 10) catch return null);
while (milliseconds >= 1) {
milliseconds /= 10;
}
const seconds: f64 = @floatFromInt(time_h.my_timegm(&tm));
return seconds + milliseconds;
}
pub const CharacterList = struct {
allocator: Allocator,
items: []Character,
pub fn deinit(self: CharacterList) void {
for (self.items) |*char| {
char.deinit();
}
self.allocator.free(self.items);
}
};
pub const Cooldown = struct {
pub const Reason = enum {
movement,
@ -339,7 +264,7 @@ pub const FightResult = struct {
drops: std.BoundedArray(DroppedItem, 8),
result: Result,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Details {
fn parse(api: *Server, obj: json.ObjectMap) !Details {
const result = try json_utils.getStringRequired(obj, "result");
var result_enum: Result = undefined;
if (std.mem.eql(u8, result, "win")) {
@ -371,14 +296,17 @@ pub const FightResult = struct {
cooldown: Cooldown,
fight: Details,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !FightResult {
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !FightResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return FightResult{
.cooldown = try Cooldown.parse(cooldown),
.fight = try Details.parse(api, fight)
.fight = try Details.parse(api, fight),
.character = try Character.parse(api, character, allocator)
};
}
@ -398,7 +326,7 @@ pub const ItemIdQuantity = struct {
id: ItemId,
quantity: u64,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemIdQuantity {
pub fn parse(api: *Server, obj: json.ObjectMap) !ItemIdQuantity {
const code = try json_utils.getStringRequired(obj, "code");
const quantity = try json_utils.getIntegerRequired(obj, "quantity");
if (quantity < 1) return error.InvalidQuantity;
@ -411,7 +339,7 @@ pub const ItemIdQuantity = struct {
};
const BoundedItems = std.BoundedArray(ItemIdQuantity, 8);
fn parseSimpleItemList(api: *ArtifactsAPI, array: json.Array) !BoundedItems {
fn parseSimpleItemList(api: *Server, array: json.Array) !BoundedItems {
var items = BoundedItems.init(0) catch unreachable;
for (array.items) |item_value| {
@ -427,7 +355,7 @@ pub const SkillResultDetails = struct {
xp: i64,
items: BoundedItems,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillResultDetails {
fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails {
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return SkillResultDetails{
@ -440,14 +368,17 @@ pub const SkillResultDetails = struct {
pub const GatherResult = struct {
cooldown: Cooldown,
details: SkillResultDetails,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatherResult {
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GatherResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return GatherResult{
.cooldown = try Cooldown.parse(cooldown),
.details = try SkillResultDetails.parse(api, details)
.details = try SkillResultDetails.parse(api, details),
.character = try Character.parse(api, character, allocator)
};
}
@ -466,14 +397,15 @@ pub const GatherResult = struct {
pub const MoveResult = struct {
cooldown: Cooldown,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult {
_ = api;
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MoveResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return MoveResult{
.cooldown = try Cooldown.parse(cooldown)
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(api, character, allocator)
};
}
@ -487,21 +419,19 @@ pub const MoveResult = struct {
else => null
};
}
pub fn deinit(self: MoveResult) void {
_ = self;
}
};
pub const GoldTransactionResult = struct {
cooldown: Cooldown,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult {
_ = api;
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GoldTransactionResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return GoldTransactionResult{
.cooldown = try Cooldown.parse(cooldown)
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(api, character, allocator)
};
}
@ -532,13 +462,15 @@ pub const GoldTransactionResult = struct {
pub const ItemTransactionResult = struct {
cooldown: Cooldown,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult {
_ = api;
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !ItemTransactionResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return ItemTransactionResult{
.cooldown = try Cooldown.parse(cooldown)
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(api, character, allocator)
};
}
@ -573,14 +505,17 @@ pub const ItemTransactionResult = struct {
pub const CraftResult = struct {
cooldown: Cooldown,
details: SkillResultDetails,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !CraftResult {
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !CraftResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return CraftResult{
.cooldown = try Cooldown.parse(cooldown),
.details = try SkillResultDetails.parse(api, details)
.details = try SkillResultDetails.parse(api, details),
.character = try Character.parse(api, character, allocator)
};
}
@ -603,7 +538,7 @@ pub const UnequipResult = struct {
cooldown: Cooldown,
item: ItemId,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !UnequipResult {
pub fn parse(api: *Server, obj: json.ObjectMap) !UnequipResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const item = json_utils.getObject(obj, "item") orelse return error.MissingProperty;
@ -635,7 +570,7 @@ pub const UnequipResult = struct {
pub const EquipResult = struct {
cooldown: Cooldown,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !EquipResult {
pub fn parse(api: *Server, obj: json.ObjectMap) !EquipResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
@ -673,7 +608,7 @@ pub const MapResult = struct {
y: i64,
content: ?MapContent,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !MapResult {
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MapResult {
_ = api;
var content: ?MapContent = null;
@ -710,7 +645,7 @@ pub const Item = struct {
quantity: u64,
items: BoundedItems,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Recipe {
pub fn parse(api: *Server, obj: json.ObjectMap) !Recipe {
const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty;
const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty;
if (level < 1) return error.InvalidLevel;
@ -740,7 +675,7 @@ pub const Item = struct {
// TODO: effects
// TODO: Grand exchange
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !Item {
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Item {
const item_obj = json_utils.getObject(obj, "item") orelse return error.MissingProperty;
const level = json_utils.getInteger(item_obj, "level") orelse return error.MissingProperty;
@ -781,20 +716,23 @@ pub const ArtifactsFetchResult = struct {
// ------------------------- General API methods ------------------------
pub fn init(allocator: Allocator) !ArtifactsAPI {
const server = try allocator.dupe(u8, "https://api.artifactsmmo.com");
const server_uri = std.Uri.parse(server) catch unreachable;
pub fn init(allocator: Allocator) !Server {
const url = try allocator.dupe(u8, "https://api.artifactsmmo.com");
const uri = std.Uri.parse(url) catch unreachable;
return ArtifactsAPI{
return Server{
.allocator = allocator,
.item_codes = std.ArrayList([]u8).init(allocator),
.client = .{ .allocator = allocator },
.server = server,
.server_uri = server_uri
.server = url,
.server_uri = uri,
.item_codes = std.ArrayList([]u8).init(allocator),
.characters = std.ArrayList(Character).init(allocator),
.items = std.StringHashMap(Item).init(allocator),
};
}
pub fn deinit(self: *ArtifactsAPI) void {
pub fn deinit(self: *Server) void {
self.client.deinit();
self.allocator.free(self.server);
if (self.token) |str| self.allocator.free(str);
@ -803,6 +741,17 @@ pub fn deinit(self: *ArtifactsAPI) void {
self.allocator.free(code);
}
self.item_codes.deinit();
for (self.characters.items) |*char| {
char.deinit();
}
self.characters.deinit();
var itemsIter = self.items.valueIterator();
while (itemsIter.next()) |item| {
item.deinit();
}
self.items.deinit();
}
const FetchOptions = struct {
@ -825,7 +774,7 @@ fn allocPaginationParams(allocator: Allocator, page: u64, page_size: ?u64) ![]u8
}
}
fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResult {
fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult {
const method = options.method;
const path = options.path;
const payload = options.payload;
@ -879,11 +828,11 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu
opts.headers.authorization = .{ .override = authorization_header.? };
}
std.log.debug("fetch {} {s}", .{method, path});
log.debug("fetch {} {s}", .{method, path});
const result = self.client.fetch(opts) catch return APIError.RequestFailed;
const response_body = response_storage.items;
std.log.debug("fetch result {}", .{result.status});
log.debug("fetch result {}", .{result.status});
if (result.status == .service_unavailable) {
return APIError.ServerUnavailable;
@ -952,7 +901,7 @@ fn handleFetchError(
}
fn fetchOptionalObject(
self: *ArtifactsAPI,
self: *Server,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Object: type,
@ -986,7 +935,7 @@ fn fetchOptionalObject(
}
fn fetchObject(
self: *ArtifactsAPI,
self: *Server,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Object: type,
@ -998,27 +947,8 @@ fn fetchObject(
return result orelse return APIError.RequestFailed;
}
fn ObjectList(Object: type) type {
return struct {
list: std.ArrayList(Object),
pub fn deinit(self: @This()) void {
for (self.list.items) |*item| {
if (std.meta.hasMethod(@TypeOf(item), "deinit")) {
item.deinit();
}
}
self.deinitList();
}
pub fn deinitList(self: @This()) void {
self.list.deinit();
}
};
}
fn fetchOptionalArray(
self: *ArtifactsAPI,
self: *Server,
allocator: Allocator,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
@ -1026,7 +956,7 @@ fn fetchOptionalArray(
parseObject: anytype,
parseObjectArgs: anytype,
fetchOptions: FetchOptions
) Error!?ObjectList(Object) {
) Error!?std.ArrayList(Object) {
if (@typeInfo(@TypeOf(parseObject)) != .Fn) {
@compileError("`parseObject` must be a function");
}
@ -1048,35 +978,29 @@ fn fetchOptionalArray(
return APIError.ParseFailed;
}
var object_array = ObjectList(Object){
.list = std.ArrayList(Object).init(allocator)
};
errdefer object_array.deinit();
// var array = std.ArrayList(Object).init(allocator);
// errdefer {
// if (std.meta.hasFn(Object, "deinit")) {
// for (array.items) |item| {
// if (@typeInfo(Object.deinit).Fn.args.len == 1) {
// item.deinit();
// }
// }
// }
// array.deinit();
// }
var array = std.ArrayList(Object).init(allocator);
errdefer {
if (std.meta.hasFn(Object, "deinit")) {
for (array.items) |*item| {
item.deinit();
}
}
array.deinit();
}
const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed;
for (result_data.items) |result_item| {
const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed;
const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return APIError.ParseFailed;
object_array.list.append(parsed_item) catch return APIError.OutOfMemory;
array.append(parsed_item) catch return APIError.OutOfMemory;
}
return object_array;
return array;
}
fn fetchArray(
self: *ArtifactsAPI,
self: *Server,
allocator: Allocator,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
@ -1084,12 +1008,12 @@ fn fetchArray(
parseObject: anytype,
parseObjectArgs: anytype,
fetchOptions: FetchOptions
) Error!ObjectList(Object) {
) Error!std.ArrayList(Object) {
const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions);
return result orelse return APIError.RequestFailed;
}
pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
pub fn setURL(self: *Server, url: []const u8) !void {
const url_dupe = self.allocator.dupe(u8, url);
errdefer self.allocator.free(url_dupe);
@ -1100,7 +1024,7 @@ pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
self.server_uri = uri;
}
pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void {
pub fn setToken(self: *Server, token: ?[]const u8) !void {
var new_token: ?[]u8 = null;
if (token != null) {
new_token = try self.allocator.dupe(u8, token.?);
@ -1110,7 +1034,7 @@ pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void {
self.token = new_token;
}
pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId {
pub fn getItemId(self: *Server, code: []const u8) !ItemId {
assert(code.len != 0);
for (0.., self.item_codes.items) |i, item_code| {
@ -1126,7 +1050,7 @@ pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId {
return @intCast(self.item_codes.items.len - 1);
}
pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 {
pub fn getItemCode(self: *const Server, id: ItemId) ?[]const u8 {
if (id >= self.item_codes.items.len) {
return null;
}
@ -1134,9 +1058,49 @@ pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 {
return self.item_codes.items[id];
}
pub fn getItemIdJson(self: *Server, object: json.ObjectMap, name: []const u8) !?ItemId {
const code = try json_utils.getStringRequired(object, name);
if (code.len == 0) {
return null;
}
return try self.getItemId(code);
}
fn findCharacterIndex(self: *const Server, name: []const u8) ?usize {
for (0.., self.characters.items) |i, character| {
if (std.mem.eql(u8, character.name, name)) {
return i;
}
}
return null;
}
fn addOrUpdateCharacter(self: *Server, character: Character) !void {
if (self.findCharacterIndex(character.name)) |found| {
self.characters.items[found].deinit();
self.characters.items[found] = character;
} else {
try self.characters.append(character);
}
}
pub fn findCharacter(self: *const 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);
}
// ------------------------- Endpoints ------------------------
pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus {
pub fn getServerStatus(self: *Server) !ServerStatus {
return try self.fetchObject(
APIError,
null,
@ -1146,74 +1110,97 @@ pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus {
);
}
pub fn getCharacter(self: *ArtifactsAPI, allocator: Allocator, name: []const u8) APIError!?Character {
pub fn getCharacter(self: *Server, name: []const u8) APIError!?Character {
const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name});
defer self.allocator.free(path);
return try self.fetchOptionalObject(
var maybe_character = try self.fetchOptionalObject(
APIError,
null,
Character,
Character.parse, .{ allocator },
Character.parse, .{ self.allocator },
.{ .method = .GET, .path = path }
);
if (maybe_character) |*character| {
errdefer character.deinit();
try self.addOrUpdateCharacter(character.*);
return character.*;
} else {
return null;
}
}
pub fn listMyCharacters(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Character) {
return self.fetchArray(
allocator,
pub fn listMyCharacters(self: *Server) APIError!std.ArrayList(Character) {
const characters = try self.fetchArray(
self.allocator,
APIError,
null,
Character,
Character.parse, .{ allocator },
Character.parse, .{ self.allocator },
.{ .method = .GET, .path = "/my/characters" }
);
errdefer characters.deinit();
for (characters.items) |character| {
try self.addOrUpdateCharacter(character);
}
return characters;
}
pub fn actionFight(self: *ArtifactsAPI, name: []const u8) FightError!FightResult {
pub fn actionFight(self: *Server, name: []const u8) FightError!FightResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name});
defer self.allocator.free(path);
return try self.fetchObject(
const result = try self.fetchObject(
FightError,
FightResult.parseError,
FightResult,
FightResult.parse, .{ },
FightResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionGather(self: *ArtifactsAPI, name: []const u8) GatherError!GatherResult {
pub fn actionGather(self: *Server, name: []const u8) GatherError!GatherResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name});
defer self.allocator.free(path);
return try self.fetchObject(
const result = try self.fetchObject(
GatherError,
GatherResult.parseError,
GatherResult,
GatherResult.parse, .{ },
GatherResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) MoveError!MoveResult {
pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) MoveError!MoveResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name});
defer self.allocator.free(path);
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
MoveError,
MoveResult.parseError,
MoveResult,
MoveResult.parse, .{ },
MoveResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionBankDepositGold(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
quantity: u64
) BankDepositGoldError!GoldTransactionResult {
@ -1223,17 +1210,20 @@ pub fn actionBankDepositGold(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
BankDepositGoldError,
GoldTransactionResult.parseDepositError,
GoldTransactionResult,
GoldTransactionResult.parse, .{ },
GoldTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionBankDepositItem(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
code: []const u8,
quantity: u64
@ -1244,17 +1234,20 @@ pub fn actionBankDepositItem(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
BankDepositItemError,
ItemTransactionResult.parseDepositError,
ItemTransactionResult,
ItemTransactionResult.parse, .{ },
ItemTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionBankWithdrawGold(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
quantity: u64
) BankDepositGoldError!GoldTransactionResult {
@ -1264,17 +1257,20 @@ pub fn actionBankWithdrawGold(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
BankWithdrawGoldError,
GoldTransactionResult.parseWithdrawError,
GoldTransactionResult,
GoldTransactionResult.parse, .{ },
GoldTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionBankWithdrawItem(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
code: []const u8,
quantity: u64
@ -1285,17 +1281,20 @@ pub fn actionBankWithdrawItem(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
BankWithdrawItemError,
ItemTransactionResult.parseWithdrawError,
ItemTransactionResult,
ItemTransactionResult.parse, .{ },
ItemTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionCraft(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
code: []const u8,
quantity: u64
@ -1306,17 +1305,20 @@ pub fn actionCraft(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
CraftError,
CraftResult.parseError,
CraftResult,
CraftResult.parse, .{ },
CraftResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionUnequip(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
slot: EquipmentSlot
) UnequipError!UnequipResult {
@ -1326,17 +1328,20 @@ pub fn actionUnequip(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\" }}", .{slot.name()});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
UnequipError,
UnequipResult.parseError,
UnequipResult,
UnequipResult.parse, .{ },
UnequipResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn actionEquip(
self: *ArtifactsAPI,
self: *Server,
name: []const u8,
slot: EquipmentSlot,
code: []const u8
@ -1347,16 +1352,19 @@ pub fn actionEquip(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\",\"code\":\"{s}\" }}", .{slot.name(), code});
defer self.allocator.free(payload);
return try self.fetchObject(
const result = try self.fetchObject(
EquipError,
EquipResult.parseError,
EquipResult,
EquipResult.parse, .{ },
EquipResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload }
);
try self.addOrUpdateCharacter(result.character);
return result;
}
pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 {
pub fn getBankGold(self: *Server) APIError!u64 {
const result = try self.fetch(.{ .method = .GET, .path = "/my/bank/gold" });
defer result.deinit();
@ -1374,7 +1382,7 @@ pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 {
return @intCast(quantity);
}
pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(ItemIdQuantity) {
pub fn getBankItems(self: *Server, allocator: Allocator) APIError!std.ArrayList(ItemIdQuantity) {
return self.fetchArray(
allocator,
APIError,
@ -1385,7 +1393,7 @@ pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectLi
);
}
pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64 {
pub fn getBankItemQuantity(self: *Server, code: []const u8) APIError!?u64 {
const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code});
defer self.allocator.free(query);
@ -1411,7 +1419,7 @@ pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64
return list_items[0].quantity;
}
pub fn getMap(self: *ArtifactsAPI, allocator: Allocator, x: i64, y: i64) APIError!?MapResult {
pub fn getMap(self: *Server, allocator: Allocator, x: i64, y: i64) APIError!?MapResult {
const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y});
defer self.allocator.free(path);
@ -1424,7 +1432,7 @@ pub fn getMap(self: *ArtifactsAPI, allocator: Allocator, x: i64, y: i64) APIErro
);
}
pub fn getMaps(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(MapResult) {
pub fn getMaps(self: *Server, allocator: Allocator) APIError!std.ArrayList(MapResult) {
return self.fetchArray(
allocator,
APIError,
@ -1435,19 +1443,32 @@ pub fn getMaps(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Ma
);
}
pub fn getItem(self: *ArtifactsAPI, allocator: Allocator, code: []const u8) APIError!?Item {
pub fn getItem(self: *Server, code: []const u8) APIError!?Item {
if (self.findItem(code)) |item| {
return item;
}
const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code});
defer self.allocator.free(path);
return self.fetchOptionalObject(
const result = try self.fetchOptionalObject(
APIError,
null,
Item,
Item.parse, .{ allocator },
Item.parse, .{ self.allocator },
.{ .method = .GET, .path = path }
);
}
test "parse date time" {
try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?);
if (result) |item| {
const item_id = try self.getItemId(code);
const code_owned = self.getItemCode(item_id).?;
var entry = try self.items.getOrPut(code_owned);
if (entry.found_existing) {
entry.value_ptr.deinit();
}
entry.value_ptr.* = item;
}
return result;
}

17
src/api/skill_stats.zig Normal file
View File

@ -0,0 +1,17 @@
const std = @import("std");
const json_utils = @import("./json_utils.zig");
const json = std.json;
const SkillStats = @This();
level: i64,
xp: i64,
max_xp: i64,
pub fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
return SkillStats{
.level = try json_utils.getIntegerRequired(object, level),
.xp = try json_utils.getIntegerRequired(object, xp),
.max_xp = try json_utils.getIntegerRequired(object, max_xp),
};
}

View File

@ -1,281 +0,0 @@
const std = @import("std");
const json_utils = @import("json_utils.zig");
const ArtifactsAPI = @import("artifacts.zig");
const parseDateTime = ArtifactsAPI.parseDateTime;
const ItemId = ArtifactsAPI.ItemId;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const json = std.json;
const Character = @This();
fn getItemId(api: *ArtifactsAPI, object: json.ObjectMap, name: []const u8) !?ItemId {
const code = try json_utils.getStringRequired(object, name);
if (code.len == 0) {
return null;
}
return try api.getItemId(code);
}
pub const SkillStats = struct {
level: i64,
xp: i64,
max_xp: i64,
fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
return SkillStats{
.level = try json_utils.getIntegerRequired(object, level),
.xp = try json_utils.getIntegerRequired(object, xp),
.max_xp = try json_utils.getIntegerRequired(object, max_xp),
};
}
};
pub const CombatStats = struct {
attack: i64,
damage: i64,
resistance: i64,
fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
return CombatStats{
.attack = try json_utils.getIntegerRequired(object, attack),
.damage = try json_utils.getIntegerRequired(object, damage),
.resistance = try json_utils.getIntegerRequired(object, resistance),
};
}
};
pub const Equipment = struct {
pub const Consumable = struct {
id: ?ItemId,
quantity: i64,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
return Consumable{
.id = try getItemId(api, obj, name),
.quantity = try json_utils.getIntegerRequired(obj, quantity),
};
}
};
weapon: ?ItemId,
shield: ?ItemId,
helmet: ?ItemId,
body_armor: ?ItemId,
leg_armor: ?ItemId,
boots: ?ItemId,
ring1: ?ItemId,
ring2: ?ItemId,
amulet: ?ItemId,
artifact1: ?ItemId,
artifact2: ?ItemId,
artifact3: ?ItemId,
consumable1: Consumable,
consumable2: Consumable,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Equipment {
return Equipment{
.weapon = try getItemId(api, obj, "weapon_slot"),
.shield = try getItemId(api, obj, "shield_slot"),
.helmet = try getItemId(api, obj, "helmet_slot"),
.body_armor = try getItemId(api, obj, "body_armor_slot"),
.leg_armor = try getItemId(api, obj, "leg_armor_slot"),
.boots = try getItemId(api, obj, "boots_slot"),
.ring1 = try getItemId(api, obj, "ring1_slot"),
.ring2 = try getItemId(api, obj, "ring2_slot"),
.amulet = try getItemId(api, obj, "amulet_slot"),
.artifact1 = try getItemId(api, obj, "artifact1_slot"),
.artifact2 = try getItemId(api, obj, "artifact2_slot"),
.artifact3 = try getItemId(api, obj, "artifact3_slot"),
.consumable1 = try Consumable.parse(api, obj, "consumable1_slot", "consumable1_slot_quantity"),
.consumable2 = try Consumable.parse(api, obj, "consumable2_slot", "consumable2_slot_quantity"),
};
}
};
pub const Inventory = struct {
const slot_count = 20;
pub const Slot = struct {
id: ?ItemId,
quantity: u64,
fn parse(api: *ArtifactsAPI, slot_obj: json.ObjectMap) !Slot {
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
if (quantity < 0) return error.InvalidQuantity;
return Slot{
.id = try getItemId(api, slot_obj, "code"),
.quantity = @intCast(quantity),
};
}
};
slots: [slot_count]Slot,
fn parse(api: *ArtifactsAPI, 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 ArtifactsAPI.ItemIdQuantity) void {
for (items) |item| {
self.addItem(item.id, item.quantity);
}
}
pub fn removeItems(self: *Inventory, items: []const ArtifactsAPI.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;
}
};
arena: *std.heap.ArenaAllocator,
name: []u8,
skin: []u8,
account: ?[]u8,
total_xp: i64,
gold: i64,
hp: i64,
haste: i64,
x: i64,
y: i64,
cooldown_expiration: f64,
combat: SkillStats,
mining: SkillStats,
woodcutting: SkillStats,
fishing: SkillStats,
weaponcrafting: SkillStats,
gearcrafting: SkillStats,
jewelrycrafting: SkillStats,
cooking: SkillStats,
water: CombatStats,
fire: CombatStats,
earth: CombatStats,
air: CombatStats,
equipment: Equipment,
inventory_max_items: i64,
inventory: Inventory,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, child_allocator: Allocator) !Character {
var arena = try child_allocator.create(std.heap.ArenaAllocator);
errdefer child_allocator.destroy(arena);
arena.* = std.heap.ArenaAllocator.init(child_allocator);
errdefer arena.deinit();
const allocator = arena.allocator();
const inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty;
const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") orelse return error.MissingProperty;
return Character{
.arena = arena,
.account = try json_utils.dupeString(allocator, obj, "account"),
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
.total_xp = try json_utils.getIntegerRequired(obj, "total_xp"),
.gold = try json_utils.getIntegerRequired(obj, "gold"),
.hp = try json_utils.getIntegerRequired(obj, "hp"),
.haste = try json_utils.getIntegerRequired(obj, "haste"),
.x = try json_utils.getIntegerRequired(obj, "x"),
.y = try json_utils.getIntegerRequired(obj, "y"),
.cooldown_expiration = parseDateTime(cooldown_expiration) orelse return error.InvalidDateTime,
.combat = try SkillStats.parse(obj, "level", "xp", "max_xp"),
.mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"),
.woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"),
.fishing = try SkillStats.parse(obj, "fishing_level", "fishing_xp", "fishing_max_xp"),
.weaponcrafting = try SkillStats.parse(obj, "weaponcrafting_level", "weaponcrafting_xp", "weaponcrafting_max_xp"),
.gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"),
.jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"),
.cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"),
.water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"),
.fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"),
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
.equipment = try Equipment.parse(api, obj),
.inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
.inventory = try Inventory.parse(api, inventory)
};
}
pub fn deinit(self: *Character) void {
var child_allocator = self.arena.child_allocator;
self.arena.deinit();
child_allocator.destroy(self.arena);
}
pub fn getItemCount(self: *const Character) u64 {
var count: u64 = 0;
for (self.inventory.slots) |slot| {
count += slot.quantity;
}
return count;
}

42
src/date_time/parse.zig Normal file
View File

@ -0,0 +1,42 @@
const std = @import("std");
pub fn parseDateTime(datetime: []const u8) ?f64 {
const time_h = @cImport({
@cDefine("_XOPEN_SOURCE", "700");
@cInclude("stddef.h");
@cInclude("stdio.h");
@cInclude("time.h");
@cInclude("timegm.h");
});
var buffer: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
var allocator = fba.allocator();
const datetime_z = allocator.dupeZ(u8, datetime) catch return null;
var tm: time_h.tm = .{};
const s = time_h.strptime(datetime_z, "%Y-%m-%dT%H:%M:%S.", &tm);
if (s == null) {
return null;
}
const s_len = std.mem.len(s);
if (s[s_len-1] != 'Z') {
return null;
}
const milliseconds_str = s[0..(s_len-1)];
var milliseconds: f64 = @floatFromInt(std.fmt.parseUnsigned(u32, milliseconds_str, 10) catch return null);
while (milliseconds >= 1) {
milliseconds /= 10;
}
const seconds: f64 = @floatFromInt(time_h.my_timegm(&tm));
return seconds + milliseconds;
}
test "parse date time" {
try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?);
}

View File

@ -1,45 +1,98 @@
const std = @import("std");
const assert = std.debug.assert;
const ArtifactsAPI = @import("artifacts.zig");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Position = @import("./api/position.zig");
const Server = @import("./api/server.zig");
// pub const std_options = .{ .log_level = .debug };
const Position = struct {
x: i64,
y: i64,
const QueuedAction = union(enum) {
move: Position,
fight,
gather,
deposit_gold: u64,
deposit_item: Server.ItemIdQuantity,
withdraw_item: Server.ItemIdQuantity,
craft_item: Server.ItemIdQuantity,
};
fn init(x: i64, y: i64) Position {
return Position{
.x = x,
.y = y
const CharacterBrain = struct {
name: []const u8,
routine: union (enum) {
idle,
fight: struct {
at: Position,
target: Server.ItemIdQuantity,
progress: u64 = 0,
},
gather: struct {
at: Position,
target: Server.ItemIdQuantity,
progress: u64 = 0,
},
craft: struct {
target: Server.ItemIdQuantity,
progress: u64 = 0,
},
},
action_queue: std.ArrayList(QueuedAction),
fn init(allocator: Allocator, name: []const u8) !CharacterBrain {
return CharacterBrain{
.name = try allocator.dupe(u8, name),
.routine = .idle,
.action_queue = std.ArrayList(QueuedAction).init(allocator),
};
}
fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
fn deinit(self: CharacterBrain) void {
const allocator = self.action_queue.allocator;
allocator.free(self.name);
self.action_queue.deinit();
}
};
const QueuedAction = union(enum) {
move: Position,
attack,
gather,
deposit_gold: u64,
deposit_item: ArtifactsAPI.ItemIdQuantity,
withdraw_item: ArtifactsAPI.ItemIdQuantity,
craft_item: ArtifactsAPI.ItemIdQuantity,
};
fn performNextAction(self: *CharacterBrain, api: *Server) !void {
assert(self.action_queue.items.len > 0);
const ActionQueue = std.ArrayList(QueuedAction);
const log = std.log.default;
const ManagedCharacter = struct {
character: ArtifactsAPI.Character,
action_queue: ActionQueue,
cooldown_expires_at: f64,
switch (self.action_queue.items[0]) {
.fight => {
log.debug("{s} attacks", .{self.name});
_ = try api.actionFight(self.name);
},
.move => |pos| {
log.debug("move {s} to ({}, {})", .{self.name, pos.x, pos.y});
_ = try api.actionMove(self.name, pos.x, pos.y);
},
.deposit_gold => |quantity| {
log.debug("deposit {} gold from {s}", .{quantity, self.name});
_ = try api.actionBankDepositGold(self.name, quantity);
},
.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);
},
.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);
},
.gather => {
log.debug("{s} gathers", .{self.name});
_ = try api.actionGather(self.name);
},
.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);
}
}
pub fn position(self: *const ManagedCharacter) Position {
return Position{ .x = self.character.x, .y = self.character.y };
_ = self.action_queue.orderedRemove(0);
}
};
@ -48,128 +101,100 @@ fn currentTime() f64 {
return timestamp / std.time.ms_per_s;
}
const Manager = struct {
const GoalManager = struct {
api: *Server,
allocator: Allocator,
characters: std.ArrayList(ManagedCharacter),
api: *ArtifactsAPI,
known_items: std.StringHashMap(ArtifactsAPI.Item),
characters: std.ArrayList(CharacterBrain),
expiration_margin: f64 = 0.1, // 100ms
bank_position: Position = .{ .x = 4, .y = 1 },
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
return Manager{
.allocator = allocator,
fn init(api: *Server, allocator: Allocator) GoalManager {
return GoalManager{
.api = api,
.known_items = std.StringHashMap(ArtifactsAPI.Item).init(allocator),
.characters = std.ArrayList(ManagedCharacter).init(allocator),
.allocator = allocator,
.characters = std.ArrayList(CharacterBrain).init(allocator)
};
}
fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void {
try self.characters.append(ManagedCharacter{
.character = character,
.action_queue = std.ArrayList(QueuedAction).init(self.allocator),
.cooldown_expires_at = character.cooldown_expiration
});
fn addCharacter(self: *GoalManager, name: []const u8) !void {
const character = try CharacterBrain.init(self.allocator, name);
try self.characters.append(character);
}
fn poll(self: *Manager) ?*ManagedCharacter {
if (self.characters.items.len == 0) return null;
fn deinit(self: GoalManager) void {
for (self.characters.items) |brain| {
brain.deinit();
}
self.characters.deinit();
}
var earliest_expiration = self.characters.items[0].cooldown_expires_at;
fn runNextAction(self: *GoalManager) !void {
var earliest_cooldown: f64 = 0;
var earliest_character: ?*CharacterBrain = null;
for (self.characters.items) |*brain| {
if (brain.action_queue.items.len == 0) continue;
var now = currentTime();
for (self.characters.items) |managed_character| {
earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at);
const character = self.api.findCharacter(brain.name).?;
if (earliest_character == null or earliest_cooldown > character.cooldown_expiration) {
earliest_character = brain;
earliest_cooldown = character.cooldown_expiration;
}
}
if (earliest_expiration > now) {
const duration_s = earliest_expiration - now;
if (earliest_character == null) return;
const now = currentTime();
if (earliest_cooldown > now) {
const duration_s = earliest_cooldown - now + self.expiration_margin;
const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s));
std.log.debug("waiting for {d:.3}s", .{duration_s});
std.time.sleep(std.time.ns_per_ms * duration_ms);
}
const cooldown_margin = 0.1; // 100ms
now = currentTime();
for (self.characters.items) |*managed_character| {
if (now - managed_character.cooldown_expires_at >= -cooldown_margin) {
return managed_character;
}
}
return null;
}
fn getItem(self: *Manager, code: []const u8) !?ArtifactsAPI.Item {
if (self.known_items.get(code)) |item| {
return item;
}
const maybe_item = try self.api.getItem(self.allocator, code);
if (maybe_item == null) {
std.log.warn("attempt to get item '{s}' which does not exist", .{code});
return null;
}
const item = maybe_item.?;
try self.known_items.putNoClobber(item.code, item);
return item;
}
fn deinit(self: *Manager) void {
for (self.characters.items) |managed_character| {
managed_character.action_queue.deinit();
}
self.characters.deinit();
var known_items_iter = self.known_items.valueIterator();
while (known_items_iter.next()) |item| {
item.deinit();
}
self.known_items.deinit();
}
fn getWorkstation(self: *const Manager, skill: ArtifactsAPI.Skill) Position {
_ = self;
// TODO: Find workstation using map endpoint
return switch (skill) {
.weaponcrafting => Position{ .x = 2, .y = 1 },
.gearcrafting => Position{ .x = 3, .y = 1 },
.jewelrycrafting => Position{ .x = 1, .y = 3 },
.cooking => Position{ .x = 1, .y = 1 },
.woodcutting => Position{ .x = -2, .y = -3 },
.mining => Position{ .x = 1, .y = 5 },
};
try earliest_character.?.performNextAction(self.api);
}
};
fn moveIfNeeded(char: *ManagedCharacter, pos: Position) !bool {
if (char.position().eql(pos)) {
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
return null;
}
const filename = args[1];
const cwd = std.fs.cwd();
var token_buffer: [256]u8 = undefined;
const token = try cwd.readFile(filename, &token_buffer);
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
}
fn moveIfNeeded(api: *Server, brain: *CharacterBrain, pos: Position) !bool {
const character = api.findCharacter(brain.name).?;
if (character.position.eql(pos)) {
return false;
}
try char.action_queue.append(.{ .move = pos });
try brain.action_queue.append(.{ .move = pos });
return true;
}
fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool {
const character = api.findCharacter(brain.name).?;
const action_queue = &brain.action_queue;
// Deposit items and gold to bank if full
if (character.getItemCount() == 0) {
return false;
}
var character_pos = managed_char.position();
if (!character_pos.eql(manager.bank_position)) {
try action_queue.append(.{ .move = manager.bank_position });
if (!character.position.eql(bank_position)) {
try action_queue.append(.{ .move = bank_position });
}
for (character.inventory.slots) |slot| {
@ -185,52 +210,51 @@ fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool
return true;
}
fn depositIfFull(manager: *Manager, char: *ManagedCharacter) !bool {
const character = char.character;
fn depositIfFull(api: *Server, brain: *CharacterBrain) !bool {
const character = api.findCharacter(brain.name).?;
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
_ = try depositItemsToBank(manager, char);
_ = try depositItemsToBank(api, brain);
if (character.gold > 0) {
try char.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
try brain.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
}
return true;
}
fn attackChickenRoutine(manager: *Manager, managed_char: *ManagedCharacter) !void {
const chicken_pos = Position{ .x = 0, .y = 1 };
// Deposit items and gold to bank if full
if (try depositIfFull(manager, managed_char)) {
fn fightRoutine(api: *Server, brain: *CharacterBrain, enemy_position: Position) !void {
if (try depositIfFull(api, brain)) {
return;
}
// Go to chickens
if (try moveIfNeeded(managed_char, chicken_pos)) {
if (try moveIfNeeded(api, brain, enemy_position)) {
return;
}
// Attack chickens
try managed_char.action_queue.append(.{ .attack = {} });
try brain.action_queue.append(.{ .fight = {} });
}
fn gatherResourceRoutine(manager: *Manager, managed_char: *ManagedCharacter, resource_pos: Position) !void {
if (try depositIfFull(manager, managed_char)) {
fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Position) !void {
if (try depositIfFull(api, brain)) {
return;
}
if (try moveIfNeeded(managed_char, resource_pos)) {
if (try moveIfNeeded(api, brain, resource_position)) {
return;
}
try managed_char.action_queue;.append(.{ .gather = {} });
try brain.action_queue.append(.{ .gather = {} });
}
fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const ArtifactsAPI.ItemIdQuantity) !bool {
fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.ItemIdQuantity) !bool {
var character = api.findCharacter(brain.name).?;
var has_all_items = true;
for (items) |item_quantity| {
const inventory_quantity = char.character.inventory.getItem(item_quantity.id);
const inventory_quantity = character.inventory.getItem(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
@ -238,14 +262,14 @@ fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const A
}
if (has_all_items) return false;
if (try moveIfNeeded(char, manager.bank_position)) {
if (try moveIfNeeded(api, brain, bank_position)) {
return true;
}
for (items) |item_quantity| {
const inventory_quantity = char.character.inventory.getItem(item_quantity.id);
const inventory_quantity = character.inventory.getItem(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try char.action_queue.append(.{ .withdraw_item = .{
try brain.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity,
}});
@ -255,26 +279,37 @@ fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const A
return true;
}
fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool {
const inventory_quantity = char.character.inventory.getItem(id);
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);
if (inventory_quantity >= quantity) {
return false;
}
const code = manager.api.getItemCode(id) orelse return error.InvalidItemId;
const item = try manager.getItem(code) orelse return error.ItemNotFound;
const code = api.getItemCode(id) orelse return error.InvalidItemId;
const item = try api.getItem(code) orelse return error.ItemNotFound;
if (item.craft == null) {
return error.NotCraftable;
}
const recipe = item.craft.?;
const workstation = manager.getWorkstation(recipe.skill);
if (try moveIfNeeded(char, workstation)) {
// TODO: Figure this out dynamically
const workstation = switch (recipe.skill) {
.weaponcrafting => Position{ .x = 2, .y = 1 },
.gearcrafting => Position{ .x = 3, .y = 1 },
.jewelrycrafting => Position{ .x = 1, .y = 3 },
.cooking => Position{ .x = 1, .y = 1 },
.woodcutting => Position{ .x = -2, .y = -3 },
.mining => Position{ .x = 1, .y = 5 },
};
if (try moveIfNeeded(api, brain, workstation)) {
return true;
}
try char.action_queue.append(.{ .craft_item = .{
try brain.action_queue.append(.{ .craft_item = .{
.id = id,
.quantity = quantity - inventory_quantity
}});
@ -282,14 +317,17 @@ fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId
return true;
}
fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool {
const inventory_quantity = char.character.inventory.getItem(id);
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);
if (inventory_quantity >= quantity) {
return false;
if (try depositItemsToBank(api, brain)) {
return;
}
}
const code = manager.api.getItemCode(id) orelse return error.InvalidItemId;
const target_item = try manager.getItem(code) orelse return error.ItemNotFound;
const code = api.getItemCode(id) orelse return error.InvalidItemId;
const target_item = try api.getItem(code) orelse return error.ItemNotFound;
if (target_item.craft == null) {
return error.NotCraftable;
}
@ -302,15 +340,13 @@ fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAP
needed_item.quantity *= quantity;
}
if (try withdrawFromBank(manager, char, needed_items.constSlice())) {
return true;
if (try withdrawFromBank(api, brain, needed_items.constSlice())) {
return;
}
if (try craftItem(manager, char, id, quantity)) {
return true;
if (try craftItem(api, brain, id, quantity)) {
return;
}
return true;
}
pub fn main() !void {
@ -318,145 +354,87 @@ pub fn main() !void {
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var api = try ArtifactsAPI.init(allocator);
var api = try Server.init(allocator);
defer api.deinit();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
if (args.len >= 2) {
const filename = args[1];
const cwd = std.fs.cwd();
try api.setToken(token);
var token_buffer: [256]u8 = undefined;
const token = try cwd.readFile(filename, &token_buffer);
try api.setToken(std.mem.trim(u8,token,"\n\t "));
var goal_manager = GoalManager.init(&api, allocator);
defer goal_manager.deinit();
const chars = try api.listMyCharacters();
defer chars.deinit();
for (chars.items) |char| {
try goal_manager.addCharacter(char.name);
}
if (api.token == null) {
return error.MissingToken;
}
goal_manager.characters.items[0].routine = .{
.fight = .{
.at = Position.init(0, 1),
.target = undefined
},
};
var manager = Manager.init(allocator, &api);
defer manager.deinit();
goal_manager.characters.items[1].routine = .{
.gather = .{
.at = Position.init(-1, 0),
.target = undefined
}
};
const status = try api.getServerStatus();
defer status.deinit();
goal_manager.characters.items[2].routine = .{
.gather = .{
.at = Position.init(2, 0),
.target = undefined
}
};
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
std.log.info("Characters online: {}", .{ status.characters_online });
goal_manager.characters.items[3].routine = .{
.gather = .{
.at = Position.init(4, 2),
.target = undefined
}
};
const characters = try api.listMyCharacters(allocator);
defer characters.deinit();
goal_manager.characters.items[4].routine = .{
.fight = .{
.at = Position.init(0, 1),
.target = undefined
},
};
// for (characters.list.items) |character| {
// try manager.addCharacter(character);
// }
try manager.addCharacter(characters.list.items[0]);
// goal_manager.characters.items[2].routine = .{
// .craft = .{
// .target = .{
// .quantity = 3,
// .id = try api.getItemId("copper"),
// }
// }
// };
std.log.info("Starting main loop", .{});
while (manager.poll()) |char| {
if (char.action_queue.items.len > 0) {
const action = char.action_queue.items[0];
while (true) {
try goal_manager.runNextAction();
var cooldown: ArtifactsAPI.Cooldown = undefined;
switch (action) {
.attack => {
std.log.debug("{s} attacks", .{char.character.name});
var result = try api.actionFight(char.character.name);
for (goal_manager.characters.items) |*character| {
if (character.action_queue.items.len > 0) continue;
cooldown = result.cooldown;
char.character.gold += result.fight.gold;
for (result.fight.drops.slice()) |item| {
char.character.inventory.addItem(item.id, @intCast(item.quantity));
}
switch (character.routine) {
.idle => {},
.fight => |args| {
try fightRoutine(&api, character, args.at);
},
.move => |pos| {
std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y});
var result = try api.actionMove(char.character.name, pos.x, pos.y);
defer result.deinit();
cooldown = result.cooldown;
char.character.x = pos.x;
char.character.y = pos.y;
.gather => |args| {
try gatherRoutine(&api, character, args.at);
},
.deposit_gold => |quantity| {
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name});
const result = try api.actionBankDepositGold(char.character.name, quantity);
cooldown = result.cooldown;
char.character.gold -= @intCast(quantity);
assert(char.character.gold >= 0);
},
.deposit_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
std.log.debug("deposit {s} (x{}) from {s}", .{code, item.quantity, char.character.name});
const result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
cooldown = result.cooldown;
char.character.inventory.removeItem(item.id, item.quantity);
},
.withdraw_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
std.log.debug("withdraw {s} (x{}) from {s}", .{code, item.quantity, char.character.name});
const result = try api.actionBankWithdrawItem(char.character.name, code, item.quantity);
cooldown = result.cooldown;
char.character.inventory.addItem(item.id, item.quantity);
},
.gather => {
std.log.debug("{s} gathers", .{char.character.name});
var result = try api.actionGather(char.character.name);
cooldown = result.cooldown;
char.character.inventory.addItems(result.details.items.slice());
},
.craft_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
std.log.debug("craft {s} (x{}) from {s}", .{code, item.quantity, char.character.name});
var result = try api.actionCraft(char.character.name, code, item.quantity);
cooldown = result.cooldown;
var inventory = &char.character.inventory;
const item_details = (try manager.getItem(code)) orelse return error.ItemNotFound;
const recipe = item_details.craft orelse return error.RecipeNotFound;
for (recipe.items.slice()) |recipe_item| {
inventory.removeItem(recipe_item.id, recipe_item.quantity * item.quantity);
}
inventory.addItems(result.details.items.slice());
.craft => |args| {
try craftRoutine(&api, character, args.target.id, args.target.quantity);
}
}
char.cooldown_expires_at = cooldown.expiration;
_ = char.action_queue.orderedRemove(0);
continue;
}
// TODO: Add checking if character state is in sync. Debug mode only
// if (try craftItemFromBank(&manager, char, try api.getItemId("copper"), 10)) {
// continue;
// }
//
// if (try depositItemsToBank(&manager, char)) {
// continue;
// }
if (std.mem.eql(u8, char.character.name, "Devin")) {
try gatherResourceRoutine(char, .{ .x = -1, .y = 0 }); // Ash trees
} else if (std.mem.eql(u8, char.character.name, "Dawn")) {
try gatherResourceRoutine(char, .{ .x = 2, .y = 0 }); // Copper ore
} else if (std.mem.eql(u8, char.character.name, "Diana")) {
try gatherResourceRoutine(char, .{ .x = 4, .y = 2 }); // Gudgeon fish
} else {
try attackChickenRoutine(char);
}
}
}