Compare commits

...

5 Commits

Author SHA1 Message Date
805e2dcabc refactor main loop 2024-08-28 01:20:28 +03:00
b33754efb8 add item crating 2024-08-10 15:13:08 +03:00
7a1b7971a9 add getting of map 2024-08-10 15:13:08 +03:00
7ac17313d1 add getting items in bank 2024-08-10 00:12:45 +03:00
409c6c9014 add getting gold in bank 2024-08-09 21:11:43 +03:00
15 changed files with 2211 additions and 1527 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;
}

1474
src/api/server.zig Normal file

File diff suppressed because it is too large Load Diff

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),
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,258 +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: i64,
fn parse(api: *ArtifactsAPI, slot_obj: json.ObjectMap) !Slot {
return Slot{
.id = try getItemId(api, slot_obj, "code"),
.quantity = try json_utils.getIntegerRequired(slot_obj, "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 -= @intCast(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 += @intCast(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 = @intCast(quantity);
}
}
};
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(obj: json.ObjectMap, child_allocator: Allocator, api: *ArtifactsAPI) !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) u32 {
var count: u32 = 0;
for (self.inventory.slots) |slot| {
count += @intCast(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,40 +1,99 @@
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,
depositGold: u64,
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
};
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);
}
}
_ = self.action_queue.orderedRemove(0);
}
};
fn currentTime() f64 {
@ -42,78 +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,
characters: std.ArrayList(CharacterBrain),
expiration_margin: f64 = 0.1, // 100ms
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
return Manager{
.allocator = allocator,
fn init(api: *Server, allocator: Allocator) GoalManager {
return GoalManager{
.api = api,
.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 deinit(self: Manager) void {
for (self.characters.items) |managed_character| {
managed_character.action_queue.deinit();
}
self.characters.deinit();
try earliest_character.?.performNextAction(self.api);
}
};
fn depositIfFull(managed_char: *ManagedCharacter) !bool {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
// Deposit items and gold to bank if full
if (character.getItemCount() < character.inventory_max_items) {
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;
}
var character_pos = Position.init(character.x, character.y);
const bank_pos = Position{ .x = 4, .y = 1 };
try brain.action_queue.append(.{ .move = pos });
if (!character_pos.eql(bank_pos)) {
try action_queue.append(.{ .move = bank_pos });
return true;
}
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;
}
if (!character.position.eql(bank_position)) {
try action_queue.append(.{ .move = bank_position });
}
for (character.inventory.slots) |slot| {
@ -121,58 +202,151 @@ fn depositIfFull(managed_char: *ManagedCharacter) !bool {
if (slot.id) |item_id| {
try action_queue.append(.{
.depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
.deposit_item = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
});
}
}
if (character.gold > 0) {
try action_queue.append(.{ .depositGold = @intCast(character.gold) });
}
return true;
}
fn attackChickenRoutine(managed_char: *ManagedCharacter) !void {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
const chicken_pos = Position{ .x = 0, .y = 1 };
var character_pos = Position.init(character.x, character.y);
// Deposit items and gold to bank if full
if (try depositIfFull(managed_char)) {
return;
fn depositIfFull(api: *Server, brain: *CharacterBrain) !bool {
const character = api.findCharacter(brain.name).?;
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
// Go to chickens
if (!character_pos.eql(chicken_pos)) {
try action_queue.append(.{ .move = chicken_pos });
return;
_ = try depositItemsToBank(api, brain);
if (character.gold > 0) {
try brain.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
}
// Attack chickens
try action_queue.append(.{ .attack = {} });
return true;
}
fn gatherResourceRoutine(managed_char: *ManagedCharacter, resource_pos: Position) !void {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
var character_pos = Position.init(character.x, character.y);
// Deposit items and gold to bank if full
if (try depositIfFull(managed_char)) {
fn fightRoutine(api: *Server, brain: *CharacterBrain, enemy_position: Position) !void {
if (try depositIfFull(api, brain)) {
return;
}
if (!character_pos.eql(resource_pos)) {
try action_queue.append(.{ .move = resource_pos });
if (try moveIfNeeded(api, brain, enemy_position)) {
return;
}
try action_queue.append(.{ .gather = {} });
try brain.action_queue.append(.{ .fight = {} });
}
fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Position) !void {
if (try depositIfFull(api, brain)) {
return;
}
if (try moveIfNeeded(api, brain, resource_position)) {
return;
}
try brain.action_queue.append(.{ .gather = {} });
}
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 = character.inventory.getItem(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
}
}
if (has_all_items) return false;
if (try moveIfNeeded(api, brain, bank_position)) {
return true;
}
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getItem(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try brain.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity,
}});
}
}
return true;
}
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 = 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.?;
// 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 brain.action_queue.append(.{ .craft_item = .{
.id = id,
.quantity = quantity - inventory_quantity
}});
return true;
}
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) {
if (try depositItemsToBank(api, brain)) {
return;
}
}
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;
}
const recipe = target_item.craft.?;
assert(recipe.quantity == 1); // TODO: Add support for recipe which produce multiple items
var needed_items = recipe.items;
for (needed_items.slice()) |*needed_item| {
needed_item.quantity *= quantity;
}
if (try withdrawFromBank(api, brain, needed_items.constSlice())) {
return;
}
if (try craftItem(api, brain, id, quantity)) {
return;
}
}
pub fn main() !void {
@ -180,112 +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 });
const characters = try api.listMyCharacters();
defer characters.deinit();
for (characters.items) |character| {
try manager.addCharacter(character);
}
goal_manager.characters.items[3].routine = .{
.gather = .{
.at = Position.init(4, 2),
.target = undefined
}
};
goal_manager.characters.items[4].routine = .{
.fight = .{
.at = Position.init(0, 1),
.target = undefined
},
};
// 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);
},
.depositGold => |quantity| {
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name});
var result = try api.actionBankDepositGold(char.character.name, quantity);
defer result.deinit();
cooldown = result.cooldown;
char.character.gold -= @intCast(quantity);
assert(char.character.gold >= 0);
},
.depositItem => |item| {
std.log.debug("deposit {s}(x{}) from {s}", .{api.getItemCode(item.id).?, item.quantity, char.character.name});
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
var result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
defer result.deinit();
cooldown = result.cooldown;
char.character.inventory.removeItem(item.id, item.quantity);
},
.gather => {
std.log.debug("{s} gathers", .{char.character.name});
var result = try api.actionGather(char.character.name);
cooldown = result.cooldown;
for (result.details.items.slice()) |item| {
char.character.inventory.addItem(item.id, @intCast(item.quantity));
}
.craft => |args| {
try craftRoutine(&api, character, args.target.id, args.target.quantity);
}
}
char.cooldown_expires_at = cooldown.expiration;
_ = char.action_queue.orderedRemove(0);
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);
}
}
}