track state of inventory locally
This commit is contained in:
parent
72d70c5d43
commit
99aad39c57
@ -10,6 +10,9 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe.linkLibC();
|
||||
exe.addIncludePath(b.path("src"));
|
||||
exe.addCSourceFile(.{ .file = b.path("src/timegm.c") });
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
|
@ -3,31 +3,44 @@ const assert = std.debug.assert;
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const json_utils = @import("json_utils.zig");
|
||||
pub const Character = @import("character.zig");
|
||||
|
||||
// Specification: https://api.artifactsmmo.com/docs
|
||||
|
||||
// TODO: Convert 'expiration' date time strings into date time objects
|
||||
|
||||
const ArtifactsAPI = @This();
|
||||
|
||||
pub const ItemId = u32;
|
||||
|
||||
allocator: Allocator,
|
||||
client: std.http.Client,
|
||||
|
||||
server: []u8,
|
||||
server_uri: std.Uri,
|
||||
|
||||
token: ?[]u8 = null,
|
||||
|
||||
item_codes: std.ArrayList([]u8),
|
||||
|
||||
pub const APIErrors = error {
|
||||
RequestFailed,
|
||||
ParseFailed
|
||||
};
|
||||
|
||||
const ServerStatus = struct {
|
||||
// TODO: Parse the rest of the fields
|
||||
allocator: Allocator,
|
||||
status: []const u8,
|
||||
version: []const u8,
|
||||
characters_online: i64,
|
||||
|
||||
fn parse(allocator: Allocator, object: json.ObjectMap) !ServerStatus {
|
||||
const status = getJsonString(object, "status") orelse return error.MissingStatus;
|
||||
const version = getJsonString(object, "version") orelse return error.MissingVersion;
|
||||
fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
|
||||
_ = api;
|
||||
|
||||
return ServerStatus{
|
||||
.allocator = allocator,
|
||||
.status = try allocator.dupe(u8, status),
|
||||
.version = try allocator.dupe(u8, version)
|
||||
.characters_online = json_utils.getInteger(object, "characters_online") orelse return error.MissingProperty,
|
||||
.status = (try json_utils.dupeString(allocator, object, "status")) orelse return error.MissingStatus,
|
||||
.version = (try json_utils.dupeString(allocator, object, "version")) orelse return error.MissingVersion
|
||||
};
|
||||
}
|
||||
|
||||
@ -37,211 +50,42 @@ const ServerStatus = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Character = struct {
|
||||
pub const SkillStats = struct {
|
||||
level: i64,
|
||||
xp: i64,
|
||||
max_xp: i64,
|
||||
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");
|
||||
});
|
||||
|
||||
fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
|
||||
return SkillStats{
|
||||
.level = getJsonInteger(object, level) orelse return error.MissingProperty,
|
||||
.xp = getJsonInteger(object, xp) orelse return error.MissingProperty,
|
||||
.max_xp = getJsonInteger(object, max_xp) orelse return error.MissingProperty,
|
||||
};
|
||||
}
|
||||
};
|
||||
var buffer: [256]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
var allocator = fba.allocator();
|
||||
|
||||
pub const CombatStats = struct {
|
||||
attack: i64,
|
||||
damage: i64,
|
||||
resistance: i64,
|
||||
const datetime_z = allocator.dupeZ(u8, datetime) catch return null;
|
||||
|
||||
fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
|
||||
return CombatStats{
|
||||
.attack = getJsonInteger(object, attack) orelse return error.MissingProperty,
|
||||
.damage = getJsonInteger(object, damage) orelse return error.MissingProperty,
|
||||
.resistance = getJsonInteger(object, resistance) orelse return error.MissingProperty,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Equipment = struct {
|
||||
pub const Consumable = struct {
|
||||
name: []u8,
|
||||
quantity: i64,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
|
||||
return Consumable{
|
||||
.name = (try dupeJsonString(allocator, obj, name)) orelse return error.MissingProperty,
|
||||
.quantity = getJsonInteger(obj, quantity) orelse return error.MissingProperty,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
weapon: []u8,
|
||||
shield: []u8,
|
||||
helmet: []u8,
|
||||
body_armor: []u8,
|
||||
leg_armor: []u8,
|
||||
boots: []u8,
|
||||
|
||||
ring1: []u8,
|
||||
ring2: []u8,
|
||||
amulet: []u8,
|
||||
|
||||
artifact1: []u8,
|
||||
artifact2: []u8,
|
||||
artifact3: []u8,
|
||||
|
||||
consumable1: Consumable,
|
||||
consumable2: Consumable,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !Equipment {
|
||||
return Equipment{
|
||||
.weapon = (try dupeJsonString(allocator, obj, "weapon_slot")) orelse return error.MissingProperty,
|
||||
.shield = (try dupeJsonString(allocator, obj, "shield_slot")) orelse return error.MissingProperty,
|
||||
.helmet = (try dupeJsonString(allocator, obj, "helmet_slot")) orelse return error.MissingProperty,
|
||||
.body_armor = (try dupeJsonString(allocator, obj, "body_armor_slot")) orelse return error.MissingProperty,
|
||||
.leg_armor = (try dupeJsonString(allocator, obj, "leg_armor_slot")) orelse return error.MissingProperty,
|
||||
.boots = (try dupeJsonString(allocator, obj, "boots_slot")) orelse return error.MissingProperty,
|
||||
.ring1 = (try dupeJsonString(allocator, obj, "ring1_slot")) orelse return error.MissingProperty,
|
||||
.ring2 = (try dupeJsonString(allocator, obj, "ring2_slot")) orelse return error.MissingProperty,
|
||||
.amulet = (try dupeJsonString(allocator, obj, "amulet_slot")) orelse return error.MissingProperty,
|
||||
.artifact1 = (try dupeJsonString(allocator, obj, "artifact1_slot")) orelse return error.MissingProperty,
|
||||
.artifact2 = (try dupeJsonString(allocator, obj, "artifact2_slot")) orelse return error.MissingProperty,
|
||||
.artifact3 = (try dupeJsonString(allocator, obj, "artifact3_slot")) orelse return error.MissingProperty,
|
||||
.consumable1 = try Consumable.parse(allocator, obj, "consumable1_slot", "consumable1_slot_quantity"),
|
||||
.consumable2 = try Consumable.parse(allocator, obj, "consumable2_slot", "consumable2_slot_quantity"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Inventory = struct {
|
||||
const slots_count = 20;
|
||||
|
||||
pub const InventorySlot = struct {
|
||||
code: []const u8,
|
||||
quantity: i64,
|
||||
|
||||
fn parse(allocator: Allocator, slot_obj: json.ObjectMap) !InventorySlot {
|
||||
return InventorySlot{
|
||||
.code = (try dupeJsonString(allocator, slot_obj, "code")) orelse return error.MissingProperty,
|
||||
.quantity = getJsonInteger(slot_obj, "quantity") orelse return error.MissingProperty,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
slots: [slots_count]InventorySlot,
|
||||
|
||||
fn parse(allocator: Allocator, slots_array: json.Array) !Inventory {
|
||||
assert(slots_array.items.len == Inventory.slots_count);
|
||||
|
||||
var inventory: Inventory = undefined;
|
||||
|
||||
for (0.., slots_array.items) |i, slot_value| {
|
||||
const slot_obj = asJsonObject(slot_value) orelse return error.InvalidType;
|
||||
inventory.slots[i] = try InventorySlot.parse(allocator, slot_obj);
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
};
|
||||
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
|
||||
name: []u8,
|
||||
skin: []u8,
|
||||
account: ?[]u8,
|
||||
total_xp: i64,
|
||||
gold: i64,
|
||||
hp: i64,
|
||||
haste: i64,
|
||||
x: i64,
|
||||
y: i64,
|
||||
cooldown: i64,
|
||||
cooldown_expiration: []u8,
|
||||
|
||||
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,
|
||||
|
||||
fn parse(child_allocator: Allocator, obj: json.ObjectMap) !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 = getJsonArray(obj, "inventory") orelse return error.MissingProperty;
|
||||
|
||||
return Character{
|
||||
.arena = arena,
|
||||
.account = try dupeJsonString(allocator, obj, "account"),
|
||||
.name = (try dupeJsonString(allocator, obj, "name")) orelse return error.MissingProperty,
|
||||
.skin = (try dupeJsonString(allocator, obj, "skin")) orelse return error.MissingProperty,
|
||||
|
||||
.total_xp = getJsonInteger(obj, "total_xp") orelse return error.MissingProperty,
|
||||
.gold = getJsonInteger(obj, "gold") orelse return error.MissingProperty,
|
||||
.hp = getJsonInteger(obj, "hp") orelse return error.MissingProperty,
|
||||
.haste = getJsonInteger(obj, "haste") orelse return error.MissingProperty,
|
||||
.x = getJsonInteger(obj, "x") orelse return error.MissingProperty,
|
||||
.y = getJsonInteger(obj, "y") orelse return error.MissingProperty,
|
||||
.cooldown = getJsonInteger(obj, "cooldown") orelse return error.MissingProperty,
|
||||
.cooldown_expiration = (try dupeJsonString(allocator, obj, "cooldown_expiration")) orelse return error.MissingProperty,
|
||||
|
||||
.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(allocator, obj),
|
||||
|
||||
.inventory_max_items = getJsonInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
|
||||
.inventory = try Inventory.parse(allocator, inventory)
|
||||
};
|
||||
var tm: time_h.tm = .{};
|
||||
const s = time_h.strptime(datetime_z, "%FT%H:%M:%S.", &tm);
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Character) void {
|
||||
var child_allocator = self.arena.child_allocator;
|
||||
self.arena.deinit();
|
||||
child_allocator.destroy(self.arena);
|
||||
const s_len = std.mem.len(s);
|
||||
if (s[s_len-1] != 'Z') {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getItemCount(self: *const Character) u32 {
|
||||
var count: u32 = 0;
|
||||
for (self.inventory.slots) |slot| {
|
||||
count += @intCast(slot.quantity);
|
||||
}
|
||||
return count;
|
||||
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,
|
||||
@ -305,44 +149,81 @@ pub const Cooldown = struct {
|
||||
}
|
||||
};
|
||||
|
||||
allocator: Allocator,
|
||||
total_seconds: i64,
|
||||
remaining_seconds: i64,
|
||||
expiration: []u8,
|
||||
expiration: f64,
|
||||
reason: Reason,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !Cooldown {
|
||||
const reason = getJsonString(obj, "reason") orelse return error.MissingProperty;
|
||||
fn parse(obj: json.ObjectMap) !Cooldown {
|
||||
const reason = try json_utils.getStringRequired(obj, "reason");
|
||||
const expiration = try json_utils.getStringRequired(obj, "expiration");
|
||||
|
||||
return Cooldown{
|
||||
.allocator = allocator,
|
||||
.total_seconds = getJsonInteger(obj, "total_seconds") orelse return error.MissingProperty,
|
||||
.remaining_seconds = getJsonInteger(obj, "remaining_seconds") orelse return error.MissingProperty,
|
||||
.expiration = (try dupeJsonString(allocator, obj, "expiration")) orelse return error.MissingProperty,
|
||||
.expiration = parseDateTime(expiration) orelse return error.InvalidDateTime,
|
||||
.reason = Reason.parse(reason) orelse return error.UnknownReason
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Cooldown) void {
|
||||
self.allocator.free(self.expiration);
|
||||
}
|
||||
};
|
||||
|
||||
pub const FightResult = struct {
|
||||
const Details = struct {
|
||||
const DroppedItem = struct {
|
||||
id: ItemId,
|
||||
quantity: i64
|
||||
};
|
||||
|
||||
const Result = enum { win, lose };
|
||||
|
||||
xp: i64,
|
||||
gold: i64,
|
||||
drops: std.BoundedArray(DroppedItem, 8),
|
||||
result: Result,
|
||||
|
||||
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Details {
|
||||
const result = try json_utils.getStringRequired(obj, "result");
|
||||
var result_enum: Result = undefined;
|
||||
if (std.mem.eql(u8, result, "win")) {
|
||||
result_enum = .win;
|
||||
} else if (std.mem.eql(u8, result, "win")) {
|
||||
result_enum = .lose;
|
||||
}
|
||||
|
||||
var drops = std.BoundedArray(DroppedItem, 8).init(0) catch unreachable;
|
||||
const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
|
||||
for (drops_obj.items) |drop_value| {
|
||||
const drop_obj = json_utils.asObject(drop_value) orelse return error.MissingProperty;
|
||||
const code = try json_utils.getStringRequired(drop_obj, "code");
|
||||
|
||||
try drops.append(DroppedItem{
|
||||
.id = try api.getItemId(code),
|
||||
.quantity = try json_utils.getIntegerRequired(drop_obj, "quantity")
|
||||
});
|
||||
}
|
||||
|
||||
return Details{
|
||||
.xp = try json_utils.getIntegerRequired(obj, "xp"),
|
||||
.gold = try json_utils.getIntegerRequired(obj, "gold"),
|
||||
.drops = drops,
|
||||
.result = result_enum,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
cooldown: Cooldown,
|
||||
character: Character,
|
||||
fight: Details,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !FightResult {
|
||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
const character = getJsonObject(obj, "character") orelse return error.MissingProperty;
|
||||
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !FightResult {
|
||||
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
|
||||
const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty;
|
||||
|
||||
return FightResult{
|
||||
.cooldown = try Cooldown.parse(allocator, cooldown),
|
||||
.character = try Character.parse(allocator, character)
|
||||
.cooldown = try Cooldown.parse(cooldown),
|
||||
.character = try Character.parse(character, allocator, api),
|
||||
.fight = try Details.parse(api, fight)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FightResult) void {
|
||||
self.cooldown.deinit();
|
||||
self.character.deinit();
|
||||
}
|
||||
};
|
||||
@ -350,48 +231,52 @@ pub const FightResult = struct {
|
||||
pub const MoveResult = struct {
|
||||
cooldown: Cooldown,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !MoveResult {
|
||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult {
|
||||
_ = api;
|
||||
|
||||
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
|
||||
return MoveResult{
|
||||
.cooldown = try Cooldown.parse(allocator, cooldown)
|
||||
.cooldown = try Cooldown.parse(cooldown)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: MoveResult) void {
|
||||
self.cooldown.deinit();
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
pub const GoldTransactionResult = struct {
|
||||
cooldown: Cooldown,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !GoldTransactionResult {
|
||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult {
|
||||
_ = api;
|
||||
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
|
||||
return GoldTransactionResult{
|
||||
.cooldown = try Cooldown.parse(allocator, cooldown)
|
||||
.cooldown = try Cooldown.parse(cooldown)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: GoldTransactionResult) void {
|
||||
self.cooldown.deinit();
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ItemTransactionResult = struct {
|
||||
cooldown: Cooldown,
|
||||
|
||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !ItemTransactionResult {
|
||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult {
|
||||
_ = api;
|
||||
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
|
||||
return ItemTransactionResult{
|
||||
.cooldown = try Cooldown.parse(allocator, cooldown)
|
||||
.cooldown = try Cooldown.parse(cooldown)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: ItemTransactionResult) void {
|
||||
self.cooldown.deinit();
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
@ -405,26 +290,30 @@ pub const ArtifactsFetchResult = struct {
|
||||
}
|
||||
};
|
||||
|
||||
allocator: Allocator,
|
||||
client: std.http.Client,
|
||||
|
||||
server: []u8,
|
||||
server_uri: std.Uri,
|
||||
|
||||
token: ?[]u8 = null,
|
||||
|
||||
pub fn init(allocator: Allocator) !ArtifactsAPI {
|
||||
const server = try allocator.dupe(u8, "https://api.artifactsmmo.com");
|
||||
const server_uri = std.Uri.parse(server) catch unreachable;
|
||||
|
||||
return ArtifactsAPI{
|
||||
.allocator = allocator,
|
||||
.item_codes = std.ArrayList([]u8).init(allocator),
|
||||
.client = .{ .allocator = allocator },
|
||||
.server = server,
|
||||
.server_uri = server_uri
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ArtifactsAPI) void {
|
||||
self.client.deinit();
|
||||
self.allocator.free(self.server);
|
||||
if (self.token) |str| self.allocator.free(str);
|
||||
|
||||
for (self.item_codes.items) |code| {
|
||||
self.allocator.free(code);
|
||||
}
|
||||
self.item_codes.deinit();
|
||||
}
|
||||
|
||||
fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload: ?[]const u8) !ArtifactsFetchResult {
|
||||
var uri = self.server_uri;
|
||||
uri.path = .{ .raw = path };
|
||||
@ -472,7 +361,7 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload
|
||||
};
|
||||
}
|
||||
|
||||
fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Method, path: []const u8, payload: ?[]const u8) !Result {
|
||||
fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Method, path: []const u8, payload: ?[]const u8, args: anytype) !Result {
|
||||
const result = try self.fetch(method, path, payload);
|
||||
defer result.deinit();
|
||||
|
||||
@ -483,8 +372,9 @@ fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Metho
|
||||
return APIErrors.ParseFailed;
|
||||
}
|
||||
|
||||
const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed;
|
||||
return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed;
|
||||
const body = json_utils.asObject(result.body.?) orelse return APIErrors.ParseFailed;
|
||||
return @call(.auto, Result.parse, .{ self, body } ++ args) catch return APIErrors.ParseFailed;
|
||||
// return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed;
|
||||
}
|
||||
|
||||
pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
|
||||
@ -508,80 +398,42 @@ pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void {
|
||||
self.token = new_token;
|
||||
}
|
||||
|
||||
fn getJsonString(object: json.ObjectMap, name: []const u8) ?[]const u8 {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId {
|
||||
assert(code.len != 0);
|
||||
|
||||
for (0.., self.item_codes.items) |i, item_code| {
|
||||
if (std.mem.eql(u8, code, item_code)) {
|
||||
return @intCast(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (value.? != json.Value.string) {
|
||||
return null;
|
||||
}
|
||||
const code_dupe = try self.allocator.dupe(u8, code);
|
||||
errdefer self.allocator.free(code_dupe);
|
||||
try self.item_codes.append(code_dupe);
|
||||
|
||||
return value.?.string;
|
||||
return @intCast(self.item_codes.items.len - 1);
|
||||
}
|
||||
|
||||
fn dupeJsonString(allocator: Allocator, object: json.ObjectMap, name: []const u8) !?[]u8 {
|
||||
const str = getJsonString(object, name) orelse return null;
|
||||
return try allocator.dupe(u8, str);
|
||||
}
|
||||
|
||||
fn getJsonInteger(object: json.ObjectMap, name: []const u8) ?i64 {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 {
|
||||
if (id >= self.item_codes.items.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.? != json.Value.integer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.?.integer;
|
||||
return self.item_codes.items[id];
|
||||
}
|
||||
|
||||
fn asJsonObject(value: json.Value) ?json.ObjectMap {
|
||||
if (value != json.Value.object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.object;
|
||||
}
|
||||
|
||||
fn asJsonArray(value: json.Value) ?json.Array {
|
||||
if (value != json.Value.array) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.array;
|
||||
}
|
||||
|
||||
fn getJsonObject(object: json.ObjectMap, name: []const u8) ?json.ObjectMap {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return asJsonObject(value.?);
|
||||
}
|
||||
|
||||
fn getJsonArray(object: json.ObjectMap, name: []const u8) ?json.Array {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return asJsonArray(value.?);
|
||||
}
|
||||
// ------------------------- Endpoints ------------------------
|
||||
|
||||
pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus {
|
||||
return try self.fetchAndParseObject(ServerStatus, .GET, "/", null);
|
||||
return try self.fetchAndParseObject(ServerStatus, .GET, "/", null, .{ self.allocator });
|
||||
}
|
||||
|
||||
pub fn getCharacter(self: *ArtifactsAPI, name: []const u8) !Character {
|
||||
const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name});
|
||||
defer self.allocator.free(path);
|
||||
|
||||
return try self.fetchAndParseObject(Character, .GET, path, null);
|
||||
return try self.fetchAndParseObject(Character, .GET, path, null, .{ self.allocator });
|
||||
}
|
||||
|
||||
pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
||||
@ -598,7 +450,7 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
||||
return APIErrors.ParseFailed;
|
||||
}
|
||||
|
||||
const body = asJsonArray(result.body.?) orelse return APIErrors.ParseFailed;
|
||||
const body = json_utils.asArray(result.body.?) orelse return APIErrors.ParseFailed;
|
||||
|
||||
var characters = try std.ArrayList(Character).initCapacity(self.allocator, body.items.len);
|
||||
errdefer {
|
||||
@ -609,13 +461,12 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
||||
}
|
||||
|
||||
for (body.items) |character_json| {
|
||||
const character_obj = asJsonObject(character_json) orelse return APIErrors.ParseFailed;
|
||||
const char = Character.parse(self.allocator, character_obj) catch return APIErrors.ParseFailed;
|
||||
const character_obj = json_utils.asObject(character_json) orelse return APIErrors.ParseFailed;
|
||||
const char = Character.parse(character_obj, self.allocator, self) catch return APIErrors.ParseFailed;
|
||||
|
||||
characters.appendAssumeCapacity(char);
|
||||
}
|
||||
|
||||
|
||||
return CharacterList{
|
||||
.allocator = self.allocator,
|
||||
.items = characters.items
|
||||
@ -626,7 +477,7 @@ pub fn actionFight(self: *ArtifactsAPI, name: []const u8) !FightResult {
|
||||
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name});
|
||||
defer self.allocator.free(path);
|
||||
|
||||
return try self.fetchAndParseObject(FightResult, .POST, path, null);
|
||||
return try self.fetchAndParseObject(FightResult, .POST, path, null, .{ self.allocator });
|
||||
}
|
||||
|
||||
pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveResult {
|
||||
@ -636,7 +487,7 @@ pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveRe
|
||||
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y});
|
||||
defer self.allocator.free(payload);
|
||||
|
||||
return try self.fetchAndParseObject(MoveResult, .POST, path, payload);
|
||||
return try self.fetchAndParseObject(MoveResult, .POST, path, payload, .{});
|
||||
}
|
||||
|
||||
pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u64) !GoldTransactionResult {
|
||||
@ -646,21 +497,15 @@ pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u6
|
||||
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
|
||||
defer self.allocator.free(payload);
|
||||
|
||||
return try self.fetchAndParseObject(GoldTransactionResult, .POST, path, payload);
|
||||
return try self.fetchAndParseObject(GoldTransactionResult, .POST, path, payload, .{});
|
||||
}
|
||||
|
||||
pub fn actionBankDeposit(self: *ArtifactsAPI, name: []const u8, code: []const u8, quantity: u64) !ItemTransactionResult {
|
||||
pub fn actionBankDepositItem(self: *ArtifactsAPI, name: []const u8, code: []const u8, quantity: u64) !ItemTransactionResult {
|
||||
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit", .{name});
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
|
||||
defer self.allocator.free(payload);
|
||||
|
||||
return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ArtifactsAPI) void {
|
||||
self.client.deinit();
|
||||
self.allocator.free(self.server);
|
||||
if (self.token) |str| self.allocator.free(str);
|
||||
return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload, .{});
|
||||
}
|
||||
|
259
src/character.zig
Normal file
259
src/character.zig
Normal file
@ -0,0 +1,259 @@
|
||||
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;
|
||||
}
|
80
src/json_utils.zig
Normal file
80
src/json_utils.zig
Normal file
@ -0,0 +1,80 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const json = std.json;
|
||||
|
||||
pub fn getInteger(object: json.ObjectMap, name: []const u8) ?i64 {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.? != json.Value.integer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.?.integer;
|
||||
}
|
||||
|
||||
pub fn getIntegerRequired(object: json.ObjectMap, name: []const u8) !i64 {
|
||||
return getInteger(object, name) orelse return error.MissingProperty;
|
||||
}
|
||||
|
||||
pub fn asObject(value: json.Value) ?json.ObjectMap {
|
||||
if (value != json.Value.object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.object;
|
||||
}
|
||||
|
||||
pub fn asArray(value: json.Value) ?json.Array {
|
||||
if (value != json.Value.array) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.array;
|
||||
}
|
||||
|
||||
pub fn getObject(object: json.ObjectMap, name: []const u8) ?json.ObjectMap {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return asObject(value.?);
|
||||
}
|
||||
|
||||
pub fn getArray(object: json.ObjectMap, name: []const u8) ?json.Array {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return asArray(value.?);
|
||||
}
|
||||
|
||||
pub fn getString(object: json.ObjectMap, name: []const u8) ?[]const u8 {
|
||||
const value = object.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.? != json.Value.string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.?.string;
|
||||
}
|
||||
|
||||
pub fn getStringRequired(object: json.ObjectMap, name: []const u8) ![]const u8 {
|
||||
return getString(object, name) orelse return error.MissingProperty;
|
||||
}
|
||||
|
||||
pub fn dupeString(allocator: Allocator, object: json.ObjectMap, name: []const u8) !?[]u8 {
|
||||
const str = getString(object, name) orelse return null;
|
||||
return try allocator.dupe(u8, str);
|
||||
}
|
||||
|
||||
pub fn dupeStringRequired(allocator: Allocator, object: json.ObjectMap, name: []const u8) ![]u8 {
|
||||
return (try dupeString(allocator, object, name)) orelse return error.MissingProperty;
|
||||
}
|
238
src/main.zig
238
src/main.zig
@ -1,42 +1,97 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const ArtifactsAPI = @import("artifacts.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
fn waitForCooldown(remaining_seconds: i64) void {
|
||||
if (remaining_seconds > 0) {
|
||||
std.log.debug("Waiting for cooldown {}", .{remaining_seconds});
|
||||
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(remaining_seconds)) + 1));
|
||||
const Position = struct {
|
||||
x: i64,
|
||||
y: i64,
|
||||
|
||||
fn init(x: i64, y: i64) Position {
|
||||
return Position{
|
||||
.x = x,
|
||||
.y = y
|
||||
};
|
||||
}
|
||||
|
||||
fn eql(self: Position, other: Position) bool {
|
||||
return self.x == other.x and self.y == other.y;
|
||||
}
|
||||
};
|
||||
|
||||
const QueuedAction = union(enum) {
|
||||
move: Position,
|
||||
attack,
|
||||
depositGold: u64,
|
||||
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
|
||||
};
|
||||
|
||||
const ManagedCharacter = struct {
|
||||
character: ArtifactsAPI.Character,
|
||||
action_queue: std.ArrayList(QueuedAction),
|
||||
cooldown_expires_at: f64,
|
||||
};
|
||||
|
||||
fn currentTime() f64 {
|
||||
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
||||
return timestamp / std.time.ms_per_s;
|
||||
}
|
||||
|
||||
pub fn depositIfNeeded(api: *ArtifactsAPI, char: *ArtifactsAPI.Character) !void {
|
||||
if (char.getItemCount() < char.inventory_max_items) return;
|
||||
const Manager = struct {
|
||||
allocator: Allocator,
|
||||
characters: std.ArrayList(ManagedCharacter),
|
||||
api: *ArtifactsAPI,
|
||||
|
||||
{
|
||||
const move_result = try api.actionMove(char.name, 4, 1);
|
||||
defer move_result.deinit();
|
||||
waitForCooldown(move_result.cooldown.remaining_seconds);
|
||||
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
|
||||
return Manager{
|
||||
.allocator = allocator,
|
||||
.api = api,
|
||||
.characters = std.ArrayList(ManagedCharacter).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
const deposit_gold = try api.actionBankDepositGold(char.name, @intCast(char.gold));
|
||||
defer deposit_gold.deinit();
|
||||
waitForCooldown(deposit_gold.cooldown.remaining_seconds);
|
||||
|
||||
for (char.inventory.slots) |slot| {
|
||||
if (slot.quantity == 0) continue;
|
||||
|
||||
const deposit_item = try api.actionBankDeposit(char.name, slot.code, @intCast(slot.quantity));
|
||||
defer deposit_item.deinit();
|
||||
waitForCooldown(deposit_item.cooldown.remaining_seconds);
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const move_result = try api.actionMove(char.name, 0, 1);
|
||||
defer move_result.deinit();
|
||||
waitForCooldown(move_result.cooldown.remaining_seconds);
|
||||
fn poll(self: *Manager) ?*ManagedCharacter {
|
||||
if (self.characters.items.len == 0) return null;
|
||||
|
||||
var earliest_expiration = self.characters.items[0].cooldown_expires_at;
|
||||
|
||||
var now = currentTime();
|
||||
for (self.characters.items) |managed_character| {
|
||||
earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at);
|
||||
}
|
||||
|
||||
if (earliest_expiration > now) {
|
||||
const duration_s = earliest_expiration - now;
|
||||
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 + 100));
|
||||
}
|
||||
|
||||
now = currentTime();
|
||||
for (self.characters.items) |*managed_character| {
|
||||
if (now >= managed_character.cooldown_expires_at) {
|
||||
return managed_character;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
fn deinit(self: Manager) void {
|
||||
for (self.characters.items) |managed_character| {
|
||||
managed_character.action_queue.deinit();
|
||||
}
|
||||
self.characters.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
@ -62,59 +117,110 @@ pub fn main() !void {
|
||||
return error.MissingToken;
|
||||
}
|
||||
|
||||
var manager = Manager.init(allocator, &api);
|
||||
defer manager.deinit();
|
||||
|
||||
const characters = try api.listMyCharacters();
|
||||
defer characters.deinit();
|
||||
|
||||
{
|
||||
var longest_cooldown: i64 = 0;
|
||||
|
||||
for (characters.items) |char| {
|
||||
if (char.x == 0 and char.y == 1) continue;
|
||||
|
||||
const result = try api.actionMove(char.name, 0, 1);
|
||||
defer result.deinit();
|
||||
|
||||
longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds);
|
||||
}
|
||||
|
||||
if (longest_cooldown > 0) {
|
||||
std.log.debug("Waiting for cooldown {}", .{longest_cooldown});
|
||||
|
||||
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1));
|
||||
}
|
||||
for (characters.items) |character| {
|
||||
try manager.addCharacter(character);
|
||||
}
|
||||
|
||||
std.log.info("Started main loop", .{});
|
||||
while (true) {
|
||||
var results = std.ArrayList(ArtifactsAPI.FightResult).init(allocator);
|
||||
defer {
|
||||
for (results.items) |*result| {
|
||||
result.deinit();
|
||||
}
|
||||
results.deinit();
|
||||
}
|
||||
const status = try api.getServerStatus();
|
||||
defer status.deinit();
|
||||
|
||||
for (characters.items) |char| {
|
||||
var result = try api.actionFight(char.name);
|
||||
errdefer result.deinit();
|
||||
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
|
||||
std.log.info("Characters online: {}", .{ status.characters_online });
|
||||
|
||||
try results.append(result);
|
||||
}
|
||||
const chicken_pos = Position{ .x = 0, .y = 1 };
|
||||
const bank_pos = Position{ .x = 4, .y = 1 };
|
||||
|
||||
{
|
||||
var longest_cooldown: i64 = 0;
|
||||
for (results.items) |result| {
|
||||
longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds);
|
||||
std.log.info("Starting main loop", .{});
|
||||
while (manager.poll()) |char| {
|
||||
if (char.action_queue.items.len > 0) {
|
||||
const action = char.action_queue.items[0];
|
||||
|
||||
var cooldown: ArtifactsAPI.Cooldown = undefined;
|
||||
switch (action) {
|
||||
.attack => {
|
||||
std.log.debug("{s} attacks", .{char.character.name});
|
||||
var result = try api.actionFight(char.character.name);
|
||||
defer result.deinit();
|
||||
|
||||
cooldown = result.cooldown;
|
||||
char.character.gold += result.fight.gold;
|
||||
for (result.fight.drops.slice()) |item| {
|
||||
char.character.inventory.addItem(item.id, @intCast(item.quantity));
|
||||
}
|
||||
},
|
||||
.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;
|
||||
},
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
if (longest_cooldown > 0) {
|
||||
std.log.debug("Waiting for cooldown {}", .{longest_cooldown});
|
||||
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1));
|
||||
}
|
||||
char.cooldown_expires_at = cooldown.expiration;
|
||||
|
||||
_ = char.action_queue.orderedRemove(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (results.items) |*result| {
|
||||
try depositIfNeeded(&api, &result.character);
|
||||
var character_pos = Position.init(char.character.x, char.character.y);
|
||||
|
||||
// Deposit items and gold to bank if full
|
||||
if (char.character.getItemCount() == char.character.inventory_max_items) {
|
||||
if (!character_pos.eql(bank_pos)) {
|
||||
try char.action_queue.append(.{ .move = bank_pos });
|
||||
}
|
||||
|
||||
for (char.character.inventory.slots) |slot| {
|
||||
if (slot.quantity == 0) continue;
|
||||
|
||||
if (slot.id) |item_id| {
|
||||
try char.action_queue.append(.{
|
||||
.depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (char.character.gold > 0) {
|
||||
try char.action_queue.append(.{ .depositGold = @intCast(char.character.gold) });
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Go to chickens
|
||||
if (!character_pos.eql(chicken_pos)) {
|
||||
try char.action_queue.append(.{ .move = chicken_pos });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attack chickens
|
||||
try char.action_queue.append(.{ .attack = {} });
|
||||
}
|
||||
}
|
||||
|
35
src/timegm.c
Normal file
35
src/timegm.c
Normal file
@ -0,0 +1,35 @@
|
||||
#include "time.h"
|
||||
|
||||
// From: https://stackoverflow.com/a/58037981
|
||||
|
||||
// Algorithm: http://howardhinnant.github.io/date_algorithms.html
|
||||
int days_from_epoch(int y, int m, int d)
|
||||
{
|
||||
y -= m <= 2;
|
||||
int era = y / 400;
|
||||
int yoe = y - era * 400; // [0, 399]
|
||||
int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
|
||||
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
|
||||
return era * 146097 + doe - 719468;
|
||||
}
|
||||
|
||||
// It does not modify broken-down time
|
||||
time_t my_timegm(struct tm const* t)
|
||||
{
|
||||
int year = t->tm_year + 1900;
|
||||
int month = t->tm_mon; // 0-11
|
||||
if (month > 11)
|
||||
{
|
||||
year += month / 12;
|
||||
month %= 12;
|
||||
}
|
||||
else if (month < 0)
|
||||
{
|
||||
int years_diff = (11 - month) / 12;
|
||||
year -= years_diff;
|
||||
month += 12 * years_diff;
|
||||
}
|
||||
int days_since_epoch = days_from_epoch(year, month + 1, t->tm_mday);
|
||||
|
||||
return 60 * (60 * (24L * days_since_epoch + t->tm_hour) + t->tm_min) + t->tm_sec;
|
||||
}
|
3
src/timegm.h
Normal file
3
src/timegm.h
Normal file
@ -0,0 +1,3 @@
|
||||
#include "time.h"
|
||||
|
||||
time_t my_timegm(struct tm const* t);
|
Loading…
Reference in New Issue
Block a user