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, .optimize = optimize,
}); });
exe.linkLibC(); exe.linkLibC();
exe.addIncludePath(b.path("src")); exe.addIncludePath(b.path("src/date_time"));
exe.addCSourceFile(.{ .file = b.path("src/timegm.c") }); exe.addCSourceFile(.{ .file = b.path("src/date_time/timegm.c") });
b.installArtifact(exe); 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 json = std.json;
const Allocator = std.mem.Allocator; 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"); const json_utils = @import("json_utils.zig");
pub const Character = @import("character.zig"); pub const Character = @import("character.zig");
// Specification: https://api.artifactsmmo.com/docs // Specification: https://api.artifactsmmo.com/docs
const ArtifactsAPI = @This(); const Server = @This();
const log = std.log.scoped(.api);
pub const ItemId = u32; pub const ItemId = u32;
allocator: Allocator, allocator: Allocator,
@ -21,6 +25,8 @@ server_uri: std.Uri,
token: ?[]u8 = null, token: ?[]u8 = null,
item_codes: std.ArrayList([]u8), item_codes: std.ArrayList([]u8),
characters: std.ArrayList(Character),
items: std.StringHashMap(Item),
// ------------------------- API errors ------------------------ // ------------------------- API errors ------------------------
@ -126,39 +132,7 @@ pub const EquipError = APIError || error {
// ------------------------- API result structs ------------------------ // ------------------------- API result structs ------------------------
pub const EquipmentSlot = enum { pub const EquipmentSlot = @import("./equipment.zig").Slot;
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 Skill = enum { pub const Skill = enum {
weaponcrafting, weaponcrafting,
@ -198,7 +172,7 @@ const ServerStatus = struct {
version: []const u8, version: []const u8,
characters_online: i64, 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; _ = api;
return ServerStatus{ 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 Cooldown = struct {
pub const Reason = enum { pub const Reason = enum {
movement, movement,
@ -339,7 +264,7 @@ pub const FightResult = struct {
drops: std.BoundedArray(DroppedItem, 8), drops: std.BoundedArray(DroppedItem, 8),
result: Result, 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"); const result = try json_utils.getStringRequired(obj, "result");
var result_enum: Result = undefined; var result_enum: Result = undefined;
if (std.mem.eql(u8, result, "win")) { if (std.mem.eql(u8, result, "win")) {
@ -371,14 +296,17 @@ pub const FightResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
fight: Details, 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 cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const fight = json_utils.getObject(obj, "fight") 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{ return FightResult{
.cooldown = try Cooldown.parse(cooldown), .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, id: ItemId,
quantity: u64, 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 code = try json_utils.getStringRequired(obj, "code");
const quantity = try json_utils.getIntegerRequired(obj, "quantity"); const quantity = try json_utils.getIntegerRequired(obj, "quantity");
if (quantity < 1) return error.InvalidQuantity; if (quantity < 1) return error.InvalidQuantity;
@ -411,7 +339,7 @@ pub const ItemIdQuantity = struct {
}; };
const BoundedItems = std.BoundedArray(ItemIdQuantity, 8); 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; var items = BoundedItems.init(0) catch unreachable;
for (array.items) |item_value| { for (array.items) |item_value| {
@ -427,7 +355,7 @@ pub const SkillResultDetails = struct {
xp: i64, xp: i64,
items: BoundedItems, 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; const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return SkillResultDetails{ return SkillResultDetails{
@ -440,14 +368,17 @@ pub const SkillResultDetails = struct {
pub const GatherResult = struct { pub const GatherResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
details: SkillResultDetails, 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 cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const details = json_utils.getObject(obj, "details") 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{ return GatherResult{
.cooldown = try Cooldown.parse(cooldown), .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 { pub const MoveResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult { pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MoveResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return MoveResult{ 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 else => null
}; };
} }
pub fn deinit(self: MoveResult) void {
_ = self;
}
}; };
pub const GoldTransactionResult = struct { pub const GoldTransactionResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult { pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GoldTransactionResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return GoldTransactionResult{ 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 { pub const ItemTransactionResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
character: Character,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult { pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !ItemTransactionResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return ItemTransactionResult{ 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 { pub const CraftResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
details: SkillResultDetails, 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 cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const details = json_utils.getObject(obj, "details") 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{ return CraftResult{
.cooldown = try Cooldown.parse(cooldown), .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, cooldown: Cooldown,
item: ItemId, 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 cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const item = json_utils.getObject(obj, "item") 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 { pub const EquipResult = struct {
cooldown: Cooldown, cooldown: Cooldown,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !EquipResult { pub fn parse(api: *Server, obj: json.ObjectMap) !EquipResult {
_ = api; _ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
@ -673,7 +608,7 @@ pub const MapResult = struct {
y: i64, y: i64,
content: ?MapContent, 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; _ = api;
var content: ?MapContent = null; var content: ?MapContent = null;
@ -710,7 +645,7 @@ pub const Item = struct {
quantity: u64, quantity: u64,
items: BoundedItems, 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 skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty;
const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty; const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty;
if (level < 1) return error.InvalidLevel; if (level < 1) return error.InvalidLevel;
@ -740,7 +675,7 @@ pub const Item = struct {
// TODO: effects // TODO: effects
// TODO: Grand exchange // 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 item_obj = json_utils.getObject(obj, "item") orelse return error.MissingProperty;
const level = json_utils.getInteger(item_obj, "level") 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 ------------------------ // ------------------------- General API methods ------------------------
pub fn init(allocator: Allocator) !ArtifactsAPI { pub fn init(allocator: Allocator) !Server {
const server = try allocator.dupe(u8, "https://api.artifactsmmo.com"); const url = try allocator.dupe(u8, "https://api.artifactsmmo.com");
const server_uri = std.Uri.parse(server) catch unreachable; const uri = std.Uri.parse(url) catch unreachable;
return ArtifactsAPI{ return Server{
.allocator = allocator, .allocator = allocator,
.item_codes = std.ArrayList([]u8).init(allocator),
.client = .{ .allocator = allocator }, .client = .{ .allocator = allocator },
.server = server, .server = url,
.server_uri = server_uri .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.client.deinit();
self.allocator.free(self.server); self.allocator.free(self.server);
if (self.token) |str| self.allocator.free(str); if (self.token) |str| self.allocator.free(str);
@ -803,6 +741,17 @@ pub fn deinit(self: *ArtifactsAPI) void {
self.allocator.free(code); self.allocator.free(code);
} }
self.item_codes.deinit(); 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 { 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 method = options.method;
const path = options.path; const path = options.path;
const payload = options.payload; const payload = options.payload;
@ -879,11 +828,11 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu
opts.headers.authorization = .{ .override = authorization_header.? }; 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 result = self.client.fetch(opts) catch return APIError.RequestFailed;
const response_body = response_storage.items; const response_body = response_storage.items;
std.log.debug("fetch result {}", .{result.status}); log.debug("fetch result {}", .{result.status});
if (result.status == .service_unavailable) { if (result.status == .service_unavailable) {
return APIError.ServerUnavailable; return APIError.ServerUnavailable;
@ -952,7 +901,7 @@ fn handleFetchError(
} }
fn fetchOptionalObject( fn fetchOptionalObject(
self: *ArtifactsAPI, self: *Server,
Error: type, Error: type,
parseError: ?fn (status: std.http.Status) ?Error, parseError: ?fn (status: std.http.Status) ?Error,
Object: type, Object: type,
@ -986,7 +935,7 @@ fn fetchOptionalObject(
} }
fn fetchObject( fn fetchObject(
self: *ArtifactsAPI, self: *Server,
Error: type, Error: type,
parseError: ?fn (status: std.http.Status) ?Error, parseError: ?fn (status: std.http.Status) ?Error,
Object: type, Object: type,
@ -998,27 +947,8 @@ fn fetchObject(
return result orelse return APIError.RequestFailed; 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( fn fetchOptionalArray(
self: *ArtifactsAPI, self: *Server,
allocator: Allocator, allocator: Allocator,
Error: type, Error: type,
parseError: ?fn (status: std.http.Status) ?Error, parseError: ?fn (status: std.http.Status) ?Error,
@ -1026,7 +956,7 @@ fn fetchOptionalArray(
parseObject: anytype, parseObject: anytype,
parseObjectArgs: anytype, parseObjectArgs: anytype,
fetchOptions: FetchOptions fetchOptions: FetchOptions
) Error!?ObjectList(Object) { ) Error!?std.ArrayList(Object) {
if (@typeInfo(@TypeOf(parseObject)) != .Fn) { if (@typeInfo(@TypeOf(parseObject)) != .Fn) {
@compileError("`parseObject` must be a function"); @compileError("`parseObject` must be a function");
} }
@ -1048,35 +978,29 @@ fn fetchOptionalArray(
return APIError.ParseFailed; return APIError.ParseFailed;
} }
var object_array = ObjectList(Object){ var array = std.ArrayList(Object).init(allocator);
.list = std.ArrayList(Object).init(allocator) errdefer {
}; if (std.meta.hasFn(Object, "deinit")) {
errdefer object_array.deinit(); for (array.items) |*item| {
// var array = std.ArrayList(Object).init(allocator); item.deinit();
// errdefer { }
// if (std.meta.hasFn(Object, "deinit")) { }
// for (array.items) |item| { array.deinit();
// if (@typeInfo(Object.deinit).Fn.args.len == 1) { }
// item.deinit();
// }
// }
// }
// array.deinit();
// }
const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed; const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed;
for (result_data.items) |result_item| { for (result_data.items) |result_item| {
const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed; 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; 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( fn fetchArray(
self: *ArtifactsAPI, self: *Server,
allocator: Allocator, allocator: Allocator,
Error: type, Error: type,
parseError: ?fn (status: std.http.Status) ?Error, parseError: ?fn (status: std.http.Status) ?Error,
@ -1084,12 +1008,12 @@ fn fetchArray(
parseObject: anytype, parseObject: anytype,
parseObjectArgs: anytype, parseObjectArgs: anytype,
fetchOptions: FetchOptions fetchOptions: FetchOptions
) Error!ObjectList(Object) { ) Error!std.ArrayList(Object) {
const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions);
return result orelse return APIError.RequestFailed; 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); const url_dupe = self.allocator.dupe(u8, url);
errdefer self.allocator.free(url_dupe); errdefer self.allocator.free(url_dupe);
@ -1100,7 +1024,7 @@ pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
self.server_uri = uri; 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; var new_token: ?[]u8 = null;
if (token != null) { if (token != null) {
new_token = try self.allocator.dupe(u8, token.?); 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; 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); assert(code.len != 0);
for (0.., self.item_codes.items) |i, item_code| { 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); 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) { if (id >= self.item_codes.items.len) {
return null; return null;
} }
@ -1134,9 +1058,49 @@ pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 {
return self.item_codes.items[id]; 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 ------------------------ // ------------------------- Endpoints ------------------------
pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus { pub fn getServerStatus(self: *Server) !ServerStatus {
return try self.fetchObject( return try self.fetchObject(
APIError, APIError,
null, 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}); const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name});
defer self.allocator.free(path); defer self.allocator.free(path);
return try self.fetchOptionalObject( var maybe_character = try self.fetchOptionalObject(
APIError, APIError,
null, null,
Character, Character,
Character.parse, .{ allocator }, Character.parse, .{ self.allocator },
.{ .method = .GET, .path = path } .{ .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) { pub fn listMyCharacters(self: *Server) APIError!std.ArrayList(Character) {
return self.fetchArray( const characters = try self.fetchArray(
allocator, self.allocator,
APIError, APIError,
null, null,
Character, Character,
Character.parse, .{ allocator }, Character.parse, .{ self.allocator },
.{ .method = .GET, .path = "/my/characters" } .{ .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}); const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name});
defer self.allocator.free(path); defer self.allocator.free(path);
return try self.fetchObject( const result = try self.fetchObject(
FightError, FightError,
FightResult.parseError, FightResult.parseError,
FightResult, FightResult,
FightResult.parse, .{ }, FightResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path } .{ .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}); const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name});
defer self.allocator.free(path); defer self.allocator.free(path);
return try self.fetchObject( const result = try self.fetchObject(
GatherError, GatherError,
GatherResult.parseError, GatherResult.parseError,
GatherResult, GatherResult,
GatherResult.parse, .{ }, GatherResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path } .{ .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}); const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name});
defer self.allocator.free(path); defer self.allocator.free(path);
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
MoveError, MoveError,
MoveResult.parseError, MoveResult.parseError,
MoveResult, MoveResult,
MoveResult.parse, .{ }, MoveResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionBankDepositGold( pub fn actionBankDepositGold(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
quantity: u64 quantity: u64
) BankDepositGoldError!GoldTransactionResult { ) BankDepositGoldError!GoldTransactionResult {
@ -1223,17 +1210,20 @@ pub fn actionBankDepositGold(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
BankDepositGoldError, BankDepositGoldError,
GoldTransactionResult.parseDepositError, GoldTransactionResult.parseDepositError,
GoldTransactionResult, GoldTransactionResult,
GoldTransactionResult.parse, .{ }, GoldTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionBankDepositItem( pub fn actionBankDepositItem(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
code: []const u8, code: []const u8,
quantity: u64 quantity: u64
@ -1244,17 +1234,20 @@ pub fn actionBankDepositItem(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
BankDepositItemError, BankDepositItemError,
ItemTransactionResult.parseDepositError, ItemTransactionResult.parseDepositError,
ItemTransactionResult, ItemTransactionResult,
ItemTransactionResult.parse, .{ }, ItemTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionBankWithdrawGold( pub fn actionBankWithdrawGold(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
quantity: u64 quantity: u64
) BankDepositGoldError!GoldTransactionResult { ) BankDepositGoldError!GoldTransactionResult {
@ -1264,17 +1257,20 @@ pub fn actionBankWithdrawGold(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
BankWithdrawGoldError, BankWithdrawGoldError,
GoldTransactionResult.parseWithdrawError, GoldTransactionResult.parseWithdrawError,
GoldTransactionResult, GoldTransactionResult,
GoldTransactionResult.parse, .{ }, GoldTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionBankWithdrawItem( pub fn actionBankWithdrawItem(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
code: []const u8, code: []const u8,
quantity: u64 quantity: u64
@ -1285,17 +1281,20 @@ pub fn actionBankWithdrawItem(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
BankWithdrawItemError, BankWithdrawItemError,
ItemTransactionResult.parseWithdrawError, ItemTransactionResult.parseWithdrawError,
ItemTransactionResult, ItemTransactionResult,
ItemTransactionResult.parse, .{ }, ItemTransactionResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionCraft( pub fn actionCraft(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
code: []const u8, code: []const u8,
quantity: u64 quantity: u64
@ -1306,17 +1305,20 @@ pub fn actionCraft(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
CraftError, CraftError,
CraftResult.parseError, CraftResult.parseError,
CraftResult, CraftResult,
CraftResult.parse, .{ }, CraftResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionUnequip( pub fn actionUnequip(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
slot: EquipmentSlot slot: EquipmentSlot
) UnequipError!UnequipResult { ) UnequipError!UnequipResult {
@ -1326,17 +1328,20 @@ pub fn actionUnequip(
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\" }}", .{slot.name()}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\" }}", .{slot.name()});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
UnequipError, UnequipError,
UnequipResult.parseError, UnequipResult.parseError,
UnequipResult, UnequipResult,
UnequipResult.parse, .{ }, UnequipResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .method = .POST, .path = path, .payload = payload }
); );
try self.addOrUpdateCharacter(result.character);
return result;
} }
pub fn actionEquip( pub fn actionEquip(
self: *ArtifactsAPI, self: *Server,
name: []const u8, name: []const u8,
slot: EquipmentSlot, slot: EquipmentSlot,
code: []const u8 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}); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\",\"code\":\"{s}\" }}", .{slot.name(), code});
defer self.allocator.free(payload); defer self.allocator.free(payload);
return try self.fetchObject( const result = try self.fetchObject(
EquipError, EquipError,
EquipResult.parseError, EquipResult.parseError,
EquipResult, EquipResult,
EquipResult.parse, .{ }, EquipResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path, .payload = payload } .{ .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" }); const result = try self.fetch(.{ .method = .GET, .path = "/my/bank/gold" });
defer result.deinit(); defer result.deinit();
@ -1374,7 +1382,7 @@ pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 {
return @intCast(quantity); 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( return self.fetchArray(
allocator, allocator,
APIError, 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}); const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code});
defer self.allocator.free(query); defer self.allocator.free(query);
@ -1411,7 +1419,7 @@ pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64
return list_items[0].quantity; 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}); const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y});
defer self.allocator.free(path); 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( return self.fetchArray(
allocator, allocator,
APIError, 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}); const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code});
defer self.allocator.free(path); defer self.allocator.free(path);
return self.fetchOptionalObject( const result = try self.fetchOptionalObject(
APIError, APIError,
null, null,
Item, Item,
Item.parse, .{ allocator }, Item.parse, .{ self.allocator },
.{ .method = .GET, .path = path } .{ .method = .GET, .path = path }
); );
}
test "parse date time" { if (result) |item| {
try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?); 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 std = @import("std");
const assert = std.debug.assert;
const ArtifactsAPI = @import("artifacts.zig");
const Allocator = std.mem.Allocator; 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 }; // pub const std_options = .{ .log_level = .debug };
const Position = struct { const QueuedAction = union(enum) {
x: i64, move: Position,
y: i64, fight,
gather,
deposit_gold: u64,
deposit_item: Server.ItemIdQuantity,
withdraw_item: Server.ItemIdQuantity,
craft_item: Server.ItemIdQuantity,
};
fn init(x: i64, y: i64) Position { const CharacterBrain = struct {
return Position{ name: []const u8,
.x = x, routine: union (enum) {
.y = y 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 { fn deinit(self: CharacterBrain) void {
return self.x == other.x and self.y == other.y; const allocator = self.action_queue.allocator;
allocator.free(self.name);
self.action_queue.deinit();
} }
};
const QueuedAction = union(enum) { fn performNextAction(self: *CharacterBrain, api: *Server) !void {
move: Position, assert(self.action_queue.items.len > 0);
attack,
gather,
deposit_gold: u64,
deposit_item: ArtifactsAPI.ItemIdQuantity,
withdraw_item: ArtifactsAPI.ItemIdQuantity,
craft_item: ArtifactsAPI.ItemIdQuantity,
};
const ActionQueue = std.ArrayList(QueuedAction); const log = std.log.default;
const ManagedCharacter = struct { switch (self.action_queue.items[0]) {
character: ArtifactsAPI.Character, .fight => {
action_queue: ActionQueue, log.debug("{s} attacks", .{self.name});
cooldown_expires_at: f64, _ = 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 { _ = self.action_queue.orderedRemove(0);
return Position{ .x = self.character.x, .y = self.character.y };
} }
}; };
@ -48,128 +101,100 @@ fn currentTime() f64 {
return timestamp / std.time.ms_per_s; return timestamp / std.time.ms_per_s;
} }
const Manager = struct { const GoalManager = struct {
api: *Server,
allocator: Allocator, allocator: Allocator,
characters: std.ArrayList(ManagedCharacter), characters: std.ArrayList(CharacterBrain),
api: *ArtifactsAPI, expiration_margin: f64 = 0.1, // 100ms
known_items: std.StringHashMap(ArtifactsAPI.Item),
bank_position: Position = .{ .x = 4, .y = 1 }, fn init(api: *Server, allocator: Allocator) GoalManager {
return GoalManager{
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
return Manager{
.allocator = allocator,
.api = api, .api = api,
.known_items = std.StringHashMap(ArtifactsAPI.Item).init(allocator), .allocator = allocator,
.characters = std.ArrayList(ManagedCharacter).init(allocator), .characters = std.ArrayList(CharacterBrain).init(allocator)
}; };
} }
fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void { fn addCharacter(self: *GoalManager, name: []const u8) !void {
try self.characters.append(ManagedCharacter{ const character = try CharacterBrain.init(self.allocator, name);
.character = character, try self.characters.append(character);
.action_queue = std.ArrayList(QueuedAction).init(self.allocator),
.cooldown_expires_at = character.cooldown_expiration
});
} }
fn poll(self: *Manager) ?*ManagedCharacter { fn deinit(self: GoalManager) void {
if (self.characters.items.len == 0) return null; 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(); const character = self.api.findCharacter(brain.name).?;
for (self.characters.items) |managed_character| { if (earliest_character == null or earliest_cooldown > character.cooldown_expiration) {
earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at); earliest_character = brain;
earliest_cooldown = character.cooldown_expiration;
}
} }
if (earliest_expiration > now) { if (earliest_character == null) return;
const duration_s = earliest_expiration - now;
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)); const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s));
std.log.debug("waiting for {d:.3}s", .{duration_s}); std.log.debug("waiting for {d:.3}s", .{duration_s});
std.time.sleep(std.time.ns_per_ms * duration_ms); std.time.sleep(std.time.ns_per_ms * duration_ms);
} }
const cooldown_margin = 0.1; // 100ms try earliest_character.?.performNextAction(self.api);
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 },
};
} }
}; };
fn moveIfNeeded(char: *ManagedCharacter, pos: Position) !bool { const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
if (char.position().eql(pos)) {
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; return false;
} }
try char.action_queue.append(.{ .move = pos }); try brain.action_queue.append(.{ .move = pos });
return true; return true;
} }
fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool { fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool {
const character = managed_char.character; const character = api.findCharacter(brain.name).?;
const action_queue = &managed_char.action_queue; const action_queue = &brain.action_queue;
// Deposit items and gold to bank if full // Deposit items and gold to bank if full
if (character.getItemCount() == 0) { if (character.getItemCount() == 0) {
return false; return false;
} }
var character_pos = managed_char.position(); if (!character.position.eql(bank_position)) {
try action_queue.append(.{ .move = bank_position });
if (!character_pos.eql(manager.bank_position)) {
try action_queue.append(.{ .move = manager.bank_position });
} }
for (character.inventory.slots) |slot| { for (character.inventory.slots) |slot| {
@ -185,52 +210,51 @@ fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool
return true; return true;
} }
fn depositIfFull(manager: *Manager, char: *ManagedCharacter) !bool { fn depositIfFull(api: *Server, brain: *CharacterBrain) !bool {
const character = char.character; const character = api.findCharacter(brain.name).?;
if (character.getItemCount() < character.inventory_max_items) { if (character.getItemCount() < character.inventory_max_items) {
return false; return false;
} }
_ = try depositItemsToBank(manager, char); _ = try depositItemsToBank(api, brain);
if (character.gold > 0) { 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 { fn fightRoutine(api: *Server, brain: *CharacterBrain, enemy_position: Position) !void {
const chicken_pos = Position{ .x = 0, .y = 1 }; if (try depositIfFull(api, brain)) {
// Deposit items and gold to bank if full
if (try depositIfFull(manager, managed_char)) {
return; return;
} }
// Go to chickens if (try moveIfNeeded(api, brain, enemy_position)) {
if (try moveIfNeeded(managed_char, chicken_pos)) {
return; return;
} }
// Attack chickens try brain.action_queue.append(.{ .fight = {} });
try managed_char.action_queue.append(.{ .attack = {} });
} }
fn gatherResourceRoutine(manager: *Manager, managed_char: *ManagedCharacter, resource_pos: Position) !void { fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Position) !void {
if (try depositIfFull(manager, managed_char)) { if (try depositIfFull(api, brain)) {
return; return;
} }
if (try moveIfNeeded(managed_char, resource_pos)) { if (try moveIfNeeded(api, brain, resource_position)) {
return; 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; var has_all_items = true;
for (items) |item_quantity| { 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) { if(inventory_quantity < item_quantity.quantity) {
has_all_items = false; has_all_items = false;
break; break;
@ -238,14 +262,14 @@ fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const A
} }
if (has_all_items) return false; if (has_all_items) return false;
if (try moveIfNeeded(char, manager.bank_position)) { if (try moveIfNeeded(api, brain, bank_position)) {
return true; return true;
} }
for (items) |item_quantity| { 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) { if(inventory_quantity < item_quantity.quantity) {
try char.action_queue.append(.{ .withdraw_item = .{ try brain.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id, .id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity, .quantity = item_quantity.quantity - inventory_quantity,
}}); }});
@ -255,26 +279,37 @@ fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const A
return true; return true;
} }
fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool { fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !bool {
const inventory_quantity = char.character.inventory.getItem(id); var character = api.findCharacter(brain.name).?;
const inventory_quantity = character.inventory.getItem(id);
if (inventory_quantity >= quantity) { if (inventory_quantity >= quantity) {
return false; return false;
} }
const code = manager.api.getItemCode(id) orelse return error.InvalidItemId; const code = api.getItemCode(id) orelse return error.InvalidItemId;
const item = try manager.getItem(code) orelse return error.ItemNotFound; const item = try api.getItem(code) orelse return error.ItemNotFound;
if (item.craft == null) { if (item.craft == null) {
return error.NotCraftable; return error.NotCraftable;
} }
const recipe = item.craft.?; 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; return true;
} }
try char.action_queue.append(.{ .craft_item = .{ try brain.action_queue.append(.{ .craft_item = .{
.id = id, .id = id,
.quantity = quantity - inventory_quantity .quantity = quantity - inventory_quantity
}}); }});
@ -282,14 +317,17 @@ fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId
return true; return true;
} }
fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool { fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !void {
const inventory_quantity = char.character.inventory.getItem(id); var character = api.findCharacter(brain.name).?;
const inventory_quantity = character.inventory.getItem(id);
if (inventory_quantity >= quantity) { if (inventory_quantity >= quantity) {
return false; if (try depositItemsToBank(api, brain)) {
return;
}
} }
const code = manager.api.getItemCode(id) orelse return error.InvalidItemId; const code = api.getItemCode(id) orelse return error.InvalidItemId;
const target_item = try manager.getItem(code) orelse return error.ItemNotFound; const target_item = try api.getItem(code) orelse return error.ItemNotFound;
if (target_item.craft == null) { if (target_item.craft == null) {
return error.NotCraftable; return error.NotCraftable;
} }
@ -302,15 +340,13 @@ fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAP
needed_item.quantity *= quantity; needed_item.quantity *= quantity;
} }
if (try withdrawFromBank(manager, char, needed_items.constSlice())) { if (try withdrawFromBank(api, brain, needed_items.constSlice())) {
return true; return;
} }
if (try craftItem(manager, char, id, quantity)) { if (try craftItem(api, brain, id, quantity)) {
return true; return;
} }
return true;
} }
pub fn main() !void { pub fn main() !void {
@ -318,145 +354,87 @@ pub fn main() !void {
defer _ = gpa.deinit(); defer _ = gpa.deinit();
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var api = try ArtifactsAPI.init(allocator); var api = try Server.init(allocator);
defer api.deinit(); defer api.deinit();
const args = try std.process.argsAlloc(allocator); const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer std.process.argsFree(allocator, args); defer allocator.free(token);
if (args.len >= 2) { try api.setToken(token);
const filename = args[1];
const cwd = std.fs.cwd();
var token_buffer: [256]u8 = undefined; var goal_manager = GoalManager.init(&api, allocator);
const token = try cwd.readFile(filename, &token_buffer); defer goal_manager.deinit();
try api.setToken(std.mem.trim(u8,token,"\n\t "));
const chars = try api.listMyCharacters();
defer chars.deinit();
for (chars.items) |char| {
try goal_manager.addCharacter(char.name);
} }
if (api.token == null) { goal_manager.characters.items[0].routine = .{
return error.MissingToken; .fight = .{
} .at = Position.init(0, 1),
.target = undefined
},
};
var manager = Manager.init(allocator, &api); goal_manager.characters.items[1].routine = .{
defer manager.deinit(); .gather = .{
.at = Position.init(-1, 0),
.target = undefined
}
};
const status = try api.getServerStatus(); goal_manager.characters.items[2].routine = .{
defer status.deinit(); .gather = .{
.at = Position.init(2, 0),
.target = undefined
}
};
std.log.info("Server status: {s} v{s}", .{ status.status, status.version }); goal_manager.characters.items[3].routine = .{
std.log.info("Characters online: {}", .{ status.characters_online }); .gather = .{
.at = Position.init(4, 2),
.target = undefined
}
};
const characters = try api.listMyCharacters(allocator); goal_manager.characters.items[4].routine = .{
defer characters.deinit(); .fight = .{
.at = Position.init(0, 1),
.target = undefined
},
};
// for (characters.list.items) |character| { // goal_manager.characters.items[2].routine = .{
// try manager.addCharacter(character); // .craft = .{
// } // .target = .{
try manager.addCharacter(characters.list.items[0]); // .quantity = 3,
// .id = try api.getItemId("copper"),
// }
// }
// };
std.log.info("Starting main loop", .{}); std.log.info("Starting main loop", .{});
while (manager.poll()) |char| { while (true) {
if (char.action_queue.items.len > 0) { try goal_manager.runNextAction();
const action = char.action_queue.items[0];
var cooldown: ArtifactsAPI.Cooldown = undefined; for (goal_manager.characters.items) |*character| {
switch (action) { if (character.action_queue.items.len > 0) continue;
.attack => {
std.log.debug("{s} attacks", .{char.character.name});
var result = try api.actionFight(char.character.name);
cooldown = result.cooldown; switch (character.routine) {
char.character.gold += result.fight.gold; .idle => {},
for (result.fight.drops.slice()) |item| { .fight => |args| {
char.character.inventory.addItem(item.id, @intCast(item.quantity)); try fightRoutine(&api, character, args.at);
}
}, },
.move => |pos| { .gather => |args| {
std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y}); try gatherRoutine(&api, character, args.at);
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;
}, },
.deposit_gold => |quantity| { .craft => |args| {
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name}); try craftRoutine(&api, character, args.target.id, args.target.quantity);
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());
} }
} }
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);
} }
} }
} }