const std = @import("std"); const assert = std.debug.assert; const json = std.json; const Allocator = std.mem.Allocator; // TODO: Maybe it would be good to move date time parsing to separate module pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime; const json_utils = @import("json_utils.zig"); pub const Character = @import("character.zig"); const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray; const EnumStringUtils = @import("./enum_string_utils.zig").EnumStringUtils; pub const Slot = @import("./slot.zig"); pub const Position = @import("./position.zig"); // Specification: https://api.artifactsmmo.com/docs const Server = @This(); const log = std.log.scoped(.api); pub const ItemId = u32; allocator: Allocator, client: std.http.Client, server: []u8, server_uri: std.Uri, token: ?[]u8 = null, item_codes: std.ArrayList([]u8), characters: std.ArrayList(Character), items: std.StringHashMap(Item), maps: std.AutoHashMap(Position, MapTile), resources: std.StringHashMap(Resource), monsters: std.StringHashMap(Monster), prefetched_resources: bool = false, prefetched_maps: bool = false, prefetched_monsters: bool = false, prefetched_items: bool = false, // ------------------------- API errors ------------------------ pub const APIError = error { ServerUnavailable, RequestFailed, ParseFailed, OutOfMemory }; pub const MoveError = APIError || error { MapNotFound, CharacterIsBusy, CharacterAtDestination, CharacterNotFound, CharacterInCooldown }; pub const FightError = APIError || error { CharacterIsBusy, CharacterIsFull, CharacterNotFound, CharacterInCooldown, MonsterNotFound, }; pub const GatherError = APIError || error { CharacterIsBusy, NotEnoughSkill, CharacterIsFull, CharacterNotFound, CharacterInCooldown, ResourceNotFound }; pub const BankDepositItemError = APIError || error { ItemNotFound, BankIsBusy, NotEnoughItems, CharacterIsBusy, CharacterNotFound, CharacterInCooldown, BankNotFound }; pub const BankDepositGoldError = APIError || error { BankIsBusy, NotEnoughGold, CharacterIsBusy, CharacterNotFound, CharacterInCooldown, BankNotFound }; pub const BankWithdrawGoldError = APIError || error { BankIsBusy, NotEnoughGold, CharacterIsBusy, CharacterNotFound, CharacterInCooldown, BankNotFound }; pub const BankWithdrawItemError = APIError || error { ItemNotFound, BankIsBusy, NotEnoughItems, CharacterIsBusy, CharacterIsFull, CharacterNotFound, CharacterInCooldown, BankNotFound }; pub const CraftError = APIError || error { RecipeNotFound, NotEnoughItems, CharacterIsBusy, NotEnoughSkill, CharacterIsFull, CharacterNotFound, CharacterInCooldown, WorkshopNotFound }; pub const UnequipError = APIError || error { ItemNotFound, // TODO: Can this really occur? maybe a bug in docs CharacterIsBusy, SlotIsEmpty, CharacterIsFull, CharacterNotFound, CharacterInCooldown, }; pub const EquipError = APIError || error { ItemNotFound, SlotIsFull, CharacterIsBusy, NotEnoughSkill, CharacterNotFound, CharacterInCooldown, }; // ------------------------- API result structs ------------------------ pub const EquipmentSlot = @import("./equipment.zig").Slot; pub const Skill = enum { weaponcrafting, gearcrafting, jewelrycrafting, cooking, woodcutting, mining, }; pub const SkillUtils = EnumStringUtils(Skill, .{ .{ "weaponcrafting" , Skill.weaponcrafting }, .{ "gearcrafting" , Skill.gearcrafting }, .{ "jewelrycrafting", Skill.jewelrycrafting }, .{ "cooking" , Skill.cooking }, .{ "woodcutting" , Skill.woodcutting }, .{ "mining" , Skill.mining }, }); const ServerStatus = struct { allocator: Allocator, status: []const u8, version: []const u8, characters_online: i64, pub fn parse(api: *Server, object: json.ObjectMap, allocator: Allocator) !ServerStatus { _ = api; return ServerStatus{ .allocator = allocator, .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 }; } pub fn deinit(self: ServerStatus) void { self.allocator.free(self.status); self.allocator.free(self.version); } }; pub const Cooldown = struct { pub const Reason = enum { movement, fight, crafting, gathering, buy_ge, sell_ge, delete_item, deposit_bank, withdraw_bank, equip, unequip, task, recycling, fn parse(str: []const u8) ?Reason { const Mapping = std.ComptimeStringMap(Reason, .{ .{ "movement" , .movement }, .{ "fight" , .fight }, .{ "crafting" , .crafting }, .{ "gathering" , .gathering }, .{ "buy_ge" , .buy_ge }, .{ "sell_ge" , .sell_ge }, .{ "delete_item" , .delete_item }, .{ "deposit_bank" , .deposit_bank }, .{ "withdraw_bank", .withdraw_bank }, .{ "equip" , .equip }, .{ "unequip" , .unequip }, .{ "task" , .task }, .{ "recycling" , .recycling }, }); return Mapping.get(str); } }; expiration: f64, reason: Reason, pub fn parse(obj: json.ObjectMap) !Cooldown { const reason = try json_utils.getStringRequired(obj, "reason"); const expiration = try json_utils.getStringRequired(obj, "expiration"); return Cooldown{ .expiration = parseDateTime(expiration) orelse return error.InvalidDateTime, .reason = Reason.parse(reason) orelse return error.UnknownReason }; } }; pub const FightResult = struct { const Details = struct { const Result = enum { win, lose }; const Drops = BoundedSlotsArray(8); xp: i64, gold: i64, drops: Drops, result: Result, fn parse(api: *Server, obj: json.ObjectMap) !Details { const result = try json_utils.getStringRequired(obj, "result"); var result_enum: Result = undefined; if (std.mem.eql(u8, result, "win")) { result_enum = .win; } else if (std.mem.eql(u8, result, "win")) { result_enum = .lose; } else { return error.InvalidProperty; } const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty; return Details{ .xp = try json_utils.getIntegerRequired(obj, "xp"), .gold = try json_utils.getIntegerRequired(obj, "gold"), .drops = try Drops.parse(api, drops_obj), .result = result_enum, }; } }; cooldown: Cooldown, fight: Details, character: Character, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !FightResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty; const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return FightResult{ .cooldown = try Cooldown.parse(cooldown), .fight = try Details.parse(api, fight), .character = try Character.parse(api, character, allocator) }; } pub fn parseError(status: std.http.Status) ?FightError { return switch (@intFromEnum(status)) { 486 => FightError.CharacterIsBusy, 497 => FightError.CharacterIsFull, 498 => FightError.CharacterNotFound, 499 => FightError.CharacterInCooldown, 598 => FightError.MonsterNotFound, else => null }; } }; // TODO: Replace this with ItemSlot struct pub const ItemIdQuantity = struct { id: ItemId, quantity: u64, pub fn init(id: ItemId, quantity: u64) ItemIdQuantity { return ItemIdQuantity{ .id = id, .quantity = quantity }; } pub fn parse(api: *Server, obj: json.ObjectMap) !ItemIdQuantity { const code = try json_utils.getStringRequired(obj, "code"); const quantity = try json_utils.getIntegerRequired(obj, "quantity"); if (quantity < 1) return error.InvalidQuantity; return ItemIdQuantity{ .id = try api.getItemId(code), .quantity = @intCast(quantity) }; } }; pub const SkillResultDetails = struct { const Items = BoundedSlotsArray(8); xp: i64, items: Items, fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails { const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty; return SkillResultDetails{ .xp = try json_utils.getIntegerRequired(obj, "xp"), .items = try Items.parse(api, items), }; } }; pub const GatherResult = struct { cooldown: Cooldown, details: SkillResultDetails, character: Character, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GatherResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty; const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return GatherResult{ .cooldown = try Cooldown.parse(cooldown), .details = try SkillResultDetails.parse(api, details), .character = try Character.parse(api, character, allocator) }; } pub fn parseError(status: std.http.Status) ?GatherError { return switch (@intFromEnum(status)) { 486 => GatherError.CharacterIsBusy, 493 => GatherError.NotEnoughSkill, 497 => GatherError.CharacterIsFull, 498 => GatherError.CharacterNotFound, 499 => GatherError.CharacterInCooldown, 598 => GatherError.ResourceNotFound, else => null }; } }; pub const MoveResult = struct { cooldown: Cooldown, character: Character, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MoveResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return MoveResult{ .cooldown = try Cooldown.parse(cooldown), .character = try Character.parse(api, character, allocator) }; } pub fn parseError(status: std.http.Status) ?MoveError { return switch (@intFromEnum(status)) { 404 => MoveError.MapNotFound, 486 => MoveError.CharacterIsBusy, 490 => MoveError.CharacterAtDestination, 498 => MoveError.CharacterNotFound, 499 => MoveError.CharacterInCooldown, else => null }; } }; pub const GoldTransactionResult = struct { cooldown: Cooldown, character: Character, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GoldTransactionResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return GoldTransactionResult{ .cooldown = try Cooldown.parse(cooldown), .character = try Character.parse(api, character, allocator) }; } pub fn parseDepositError(status: std.http.Status) ?BankDepositGoldError { return switch (@intFromEnum(status)) { 461 => BankDepositGoldError.BankIsBusy, 486 => BankDepositGoldError.CharacterIsBusy, 492 => BankDepositGoldError.NotEnoughGold, 498 => BankDepositGoldError.CharacterNotFound, 499 => BankDepositGoldError.CharacterInCooldown, 598 => BankDepositGoldError.BankNotFound, else => null }; } pub fn parseWithdrawError(status: std.http.Status) ?BankWithdrawGoldError { return switch (@intFromEnum(status)) { 460 => BankWithdrawGoldError.NotEnoughGold, 461 => BankWithdrawGoldError.BankIsBusy, 486 => BankWithdrawGoldError.CharacterIsBusy, 498 => BankWithdrawGoldError.CharacterNotFound, 499 => BankWithdrawGoldError.CharacterInCooldown, 598 => BankWithdrawGoldError.BankNotFound, else => null }; } }; pub const ItemTransactionResult = struct { cooldown: Cooldown, character: Character, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !ItemTransactionResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return ItemTransactionResult{ .cooldown = try Cooldown.parse(cooldown), .character = try Character.parse(api, character, allocator) }; } pub fn parseDepositError(status: std.http.Status) ?BankDepositItemError { return switch (@intFromEnum(status)) { 404 => BankDepositItemError.ItemNotFound, 461 => BankDepositItemError.BankIsBusy, 478 => BankDepositItemError.NotEnoughItems, 486 => BankDepositItemError.CharacterIsBusy, 498 => BankDepositItemError.CharacterNotFound, 499 => BankDepositItemError.CharacterInCooldown, 598 => BankDepositItemError.BankNotFound, else => null }; } pub fn parseWithdrawError(status: std.http.Status) ?BankWithdrawItemError { return switch (@intFromEnum(status)) { 404 => BankWithdrawItemError.ItemNotFound, 461 => BankWithdrawItemError.BankIsBusy, 478 => BankWithdrawItemError.NotEnoughItems, 486 => BankWithdrawItemError.CharacterIsBusy, 497 => BankWithdrawItemError.CharacterIsFull, 498 => BankWithdrawItemError.CharacterNotFound, 499 => BankWithdrawItemError.CharacterInCooldown, 598 => BankWithdrawItemError.BankNotFound, else => null }; } }; pub const CraftResult = struct { cooldown: Cooldown, details: SkillResultDetails, character: Character, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !CraftResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty; const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return CraftResult{ .cooldown = try Cooldown.parse(cooldown), .details = try SkillResultDetails.parse(api, details), .character = try Character.parse(api, character, allocator) }; } pub fn parseError(status: std.http.Status) ?CraftError { return switch (@intFromEnum(status)) { 404 => CraftError.RecipeNotFound, 478 => CraftError.NotEnoughItems, 486 => CraftError.CharacterIsBusy, 493 => CraftError.NotEnoughSkill, 497 => CraftError.CharacterIsFull, 498 => CraftError.CharacterNotFound, 499 => CraftError.CharacterInCooldown, 598 => CraftError.WorkshopNotFound, else => null }; } }; pub const UnequipResult = struct { cooldown: Cooldown, item: ItemId, pub fn parse(api: *Server, obj: json.ObjectMap) !UnequipResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const item = json_utils.getObject(obj, "item") orelse return error.MissingProperty; const item_code = json_utils.getString(item, "code") orelse return error.MissingProperty; const item_id = try api.getItemId(item_code); // TODO: Might as well save information about time, because full details about it are given return UnequipResult{ .cooldown = try Cooldown.parse(cooldown), .item = item_id }; } pub fn parseError(status: std.http.Status) ?UnequipError { return switch (@intFromEnum(status)) { 404 => UnequipError.ItemNotFound, 486 => UnequipError.CharacterIsBusy, 491 => UnequipError.SlotIsEmpty, 497 => UnequipError.CharacterIsFull, 498 => UnequipError.CharacterNotFound, 499 => UnequipError.CharacterInCooldown, else => null }; } }; pub const EquipResult = struct { cooldown: Cooldown, pub fn parse(api: *Server, obj: json.ObjectMap) !EquipResult { _ = api; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; // TODO: Might as well save information about time, because full details about it are given return EquipResult{ .cooldown = try Cooldown.parse(cooldown) }; } pub fn parseError(status: std.http.Status) ?EquipError { return switch (@intFromEnum(status)) { 404 => EquipError.ItemNotFound, 478 => EquipError.ItemNotFound, // TODO: What is the difference between 404 and 478? 485 => EquipError.SlotIsFull, 486 => EquipError.CharacterIsBusy, 491 => EquipError.SlotIsFull, // TODO: What is the difference between 485 and 491? 496 => EquipError.NotEnoughSkill, 498 => EquipError.CharacterNotFound, 499 => EquipError.CharacterInCooldown, else => null }; } }; pub const MapContentType = enum { monster, resource, workshop, bank, grand_exchange, tasks_master, }; pub const MapContentTypeUtils = EnumStringUtils(MapContentType, .{ .{ "monster" , .monster }, .{ "resource" , .resource }, .{ "workshop" , .workshop }, .{ "bank" , .bank }, .{ "grand_exchange", .grand_exchange }, .{ "tasks_master" , .tasks_master }, }); pub const MapTile = struct { pub const MapContent = struct { type: MapContentType, code: []u8, }; name: []u8, skin: []u8, position: Position, content: ?MapContent, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MapTile { _ = api; var content: ?MapContent = null; if (json_utils.getObject(obj, "content")) |content_obj| { const content_type = json_utils.getString(content_obj, "type") orelse return error.MissingProperty; content = MapContent{ .type = MapContentTypeUtils.fromString(content_type) orelse return error.InvalidContentType, .code = (try json_utils.dupeString(allocator, content_obj, "code")) orelse return error.MissingProperty, }; } const x = json_utils.getInteger(obj, "x") orelse return error.MissingProperty; const y = json_utils.getInteger(obj, "y") orelse return error.MissingProperty; return MapTile{ .name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty, .skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty, .position = Position.init(x, y), .content = content }; } pub fn deinit(self: MapTile, allocator: Allocator) void { allocator.free(self.name); allocator.free(self.skin); if (self.content) |content| { allocator.free(content.code); } } }; pub const ItemType = enum { consumable, body_armor, weapon, resource, leg_armor, helmet, boots, shield, amulet, ring, artifact, currency, }; const ItemTypeUtils = EnumStringUtils(ItemType, .{ .{ "consumable", .consumable }, .{ "body_armor", .body_armor }, .{ "weapon" , .weapon }, .{ "resource" , .resource }, .{ "leg_armor" , .leg_armor }, .{ "helmet" , .helmet }, .{ "boots" , .boots }, .{ "shield" , .shield }, .{ "amulet" , .amulet }, .{ "ring" , .ring }, .{ "artifact" , .artifact }, .{ "currency" , .currency }, }); pub const Item = struct { pub const Recipe = struct { const Items = BoundedSlotsArray(8); skill: Skill, level: u64, quantity: u64, items: Items, pub fn parse(api: *Server, obj: json.ObjectMap) !Recipe { const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty; const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty; if (level < 1) return error.InvalidLevel; const quantity = json_utils.getInteger(obj, "quantity") orelse return error.MissingProperty; if (quantity < 1) return error.InvalidQuantity; const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty; return Recipe{ .skill = SkillUtils.fromString(skill) orelse return error.InvalidSkill, .level = @intCast(level), .quantity = @intCast(quantity), .items = try Items.parse(api, items) }; } }; allocator: Allocator, name: []u8, code: []u8, level: u64, type: ItemType, subtype: []u8, description: []u8, craft: ?Recipe, // TODO: effects pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Item { const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty; if (level < 1) return error.InvalidLevel; const craft = json_utils.getObject(obj, "craft"); const item_type_str = try json_utils.getStringRequired(obj, "type"); return Item{ .allocator = allocator, .name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty, .code = (try json_utils.dupeString(allocator, obj, "code")) orelse return error.MissingProperty, .level = @intCast(level), .type = ItemTypeUtils.fromString(item_type_str) orelse return error.InvalidType, .subtype = (try json_utils.dupeString(allocator, obj, "subtype")) orelse return error.MissingProperty, .description = (try json_utils.dupeString(allocator, obj, "description")) orelse return error.MissingProperty, .craft = if (craft != null) try Recipe.parse(api, craft.?) else null }; } pub fn deinit(self: Item) void { self.allocator.free(self.name); self.allocator.free(self.code); self.allocator.free(self.subtype); self.allocator.free(self.description); } }; pub const ItemWithGE = struct { item: Item, // TODO: Grand exchange pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !ItemWithGE { const item_obj = json_utils.getObject(obj, "item") orelse return error.MissingProperty; const ge_obj = json_utils.getObject(obj, "ge") orelse return error.MissingProperty; _ = ge_obj; return ItemWithGE{ .item = try Item.parse(api, item_obj, allocator), }; } }; pub const ResourceSkill = enum { mining, woodcutting, fishing, }; const ResourceSkillUtils = EnumStringUtils(ResourceSkill, .{ .{ "mining" , .mining }, .{ "woodcutting", .woodcutting }, .{ "fishing" , .fishing }, }); const DropRates = std.BoundedArray(DropRate, 8); const DropRate = struct { item_id: ItemId, rate: u64, min_quantity: u64, max_quantity: u64, fn parse(api: *Server, obj: json.ObjectMap) !DropRate { const rate = try json_utils.getIntegerRequired(obj, "rate"); if (rate < 1) { return error.InvalidRate; } const min_quantity = try json_utils.getIntegerRequired(obj, "min_quantity"); if (min_quantity < 1) { return error.InvalidMinQuantity; } const max_quantity = try json_utils.getIntegerRequired(obj, "max_quantity"); if (max_quantity < 1) { return error.InvalidMinQuantity; } const code_str = try json_utils.getStringRequired(obj, "code"); const item_id = try api.getItemId(code_str); return DropRate{ .item_id = item_id, .rate = @intCast(rate), .min_quantity = @intCast(min_quantity), .max_quantity = @intCast(max_quantity) }; } fn parseList(api: *Server, array: json.Array) !DropRates { var drops = DropRates.init(0) catch unreachable; for (array.items) |drop_value| { const drop_obj = json_utils.asObject(drop_value) orelse return error.InvalidObject; try drops.append(try DropRate.parse(api, drop_obj)); } return drops; } fn doesListContain(drops: *DropRates, item_id: ItemId) bool { for (drops.constSlice()) |drop| { if (drop.item_id == item_id) { return true; } } return false; } }; pub const Resource = struct { name: []u8, code: []u8, skill: ResourceSkill, level: u64, drops: DropRates, fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Resource { const drops_array = json_utils.getArray(obj, "drops") orelse return error.MissingProperty; const level = try json_utils.getIntegerRequired(obj, "level"); if (level < 0) { return error.InvalidLevel; } const skill_str = try json_utils.getStringRequired(obj, "skill"); return Resource{ .name = try json_utils.dupeStringRequired(allocator, obj, "name"), .code = try json_utils.dupeStringRequired(allocator, obj, "code"), .level = @intCast(level), .skill = ResourceSkillUtils.fromString(skill_str) orelse return error.InvalidSkill, .drops = try DropRate.parseList(api, drops_array) }; } fn deinit(self: Resource, allocator: Allocator) void { allocator.free(self.name); allocator.free(self.code); } }; pub const Monster = struct { const ElementalStats = struct { attack: i64, resistance: i64, pub fn parse(object: json.ObjectMap, attack: []const u8, resistance: []const u8) !ElementalStats { return ElementalStats{ .attack = try json_utils.getIntegerRequired(object, attack), .resistance = try json_utils.getIntegerRequired(object, resistance), }; } }; name: []u8, code: []u8, level: u64, hp: u64, min_gold: u64, max_gold: u64, fire: ElementalStats, earth: ElementalStats, water: ElementalStats, air: ElementalStats, drops: DropRates, fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Monster { const drops_array = json_utils.getArray(obj, "drops") orelse return error.MissingProperty; const min_gold = try json_utils.getIntegerRequired(obj, "min_gold"); if (min_gold < 0) { return error.InvalidMinGold; } const max_gold = try json_utils.getIntegerRequired(obj, "max_gold"); if (max_gold < 0) { return error.InvalidMaxGold; } const level = try json_utils.getIntegerRequired(obj, "level"); if (level < 0) { return error.InvalidLevel; } const hp = try json_utils.getIntegerRequired(obj, "hp"); if (hp < 0) { return error.InvalidHp; } return Monster{ .name = try json_utils.dupeStringRequired(allocator, obj, "name"), .code = try json_utils.dupeStringRequired(allocator, obj, "code"), .level = @intCast(level), .hp = @intCast(hp), .fire = try ElementalStats.parse(obj, "attack_fire" , "res_fire" ), .earth = try ElementalStats.parse(obj, "attack_earth", "res_earth"), .water = try ElementalStats.parse(obj, "attack_water", "res_water"), .air = try ElementalStats.parse(obj, "attack_air" , "res_air" ), .min_gold = @intCast(min_gold), .max_gold = @intCast(max_gold), .drops = try DropRate.parseList(api, drops_array) }; } fn deinit(self: Monster, allocator: Allocator) void { allocator.free(self.name); allocator.free(self.code); } }; pub const ArtifactsFetchResult = struct { arena: std.heap.ArenaAllocator, status: std.http.Status, body: ?json.Value = null, fn deinit(self: ArtifactsFetchResult) void { self.arena.deinit(); } }; fn appendQueryParam(query: *std.ArrayList(u8), key: []const u8, value: []const u8) !void { if (query.items.len > 0) { try query.appendSlice("&"); } try query.appendSlice(key); try query.appendSlice("="); try query.appendSlice(value); } // ------------------------- General API methods ------------------------ pub fn init(allocator: Allocator) !Server { const url = try allocator.dupe(u8, "https://api.artifactsmmo.com"); const uri = std.Uri.parse(url) catch unreachable; return Server{ .allocator = allocator, .client = .{ .allocator = allocator }, .server = url, .server_uri = uri, .item_codes = std.ArrayList([]u8).init(allocator), .characters = std.ArrayList(Character).init(allocator), .items = std.StringHashMap(Item).init(allocator), .maps = std.AutoHashMap(Position, MapTile).init(allocator), .resources = std.StringHashMap(Resource).init(allocator), .monsters = std.StringHashMap(Monster).init(allocator), }; } pub fn deinit(self: *Server) 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(); for (self.characters.items) |*char| { char.deinit(); } self.characters.deinit(); var itemsIter = self.items.valueIterator(); while (itemsIter.next()) |item| { item.deinit(); } self.items.deinit(); var mapsIter = self.maps.valueIterator(); while (mapsIter.next()) |map| { map.deinit(self.allocator); } self.maps.deinit(); var resourcesIter = self.resources.valueIterator(); while (resourcesIter.next()) |resource| { resource.deinit(self.allocator); } self.resources.deinit(); var monstersIter = self.monsters.valueIterator(); while (monstersIter.next()) |monster| { monster.deinit(self.allocator); } self.monsters.deinit(); } const FetchOptions = struct { method: std.http.Method, path: []const u8, payload: ?[]const u8 = null, query: ?[]const u8 = null, page: ?u64 = null, page_size: ?u64 = null, paginated: bool = false }; fn allocPaginationParams(allocator: Allocator, page: u64, page_size: ?u64) ![]u8 { if (page_size) |size| { return try std.fmt.allocPrint(allocator, "page={}&size={}", .{page, size}); } else { return try std.fmt.allocPrint(allocator, "page={}", .{page}); } } // TODO: add retries when hitting a ratelimit fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { const method = options.method; const path = options.path; const payload = options.payload; var uri = self.server_uri; uri.path = .{ .raw = path }; var arena = std.heap.ArenaAllocator.init(self.allocator); errdefer arena.deinit(); var result_status: std.http.Status = .ok; var result_body: ?json.Value = null; var current_page: u64 = options.page orelse 1; var total_pages: u64 = 1; var fetch_results = std.ArrayList(json.Value).init(arena.allocator()); const has_query = options.query != null and options.query.?.len > 0; while (true) : (current_page += 1) { var pagination_params: ?[]u8 = null; defer if (pagination_params) |str| self.allocator.free(str); if (options.paginated) { pagination_params = try allocPaginationParams(self.allocator, current_page, options.page_size); } if (has_query and pagination_params != null) { const combined = try std.mem.join(self.allocator, "&", &.{ options.query.?, pagination_params.? }); self.allocator.free(pagination_params.?); pagination_params = combined; uri.query = .{ .raw = combined }; } else if (pagination_params != null) { uri.query = .{ .raw = pagination_params.? }; } else if (has_query) { uri.query = .{ .raw = options.query.? }; } var response_storage = std.ArrayList(u8).init(arena.allocator()); var opts = std.http.Client.FetchOptions{ .method = method, .location = .{ .uri = uri }, .payload = payload, .response_storage = .{ .dynamic = &response_storage }, }; var authorization_header: ?[]u8 = null; defer if (authorization_header) |str| self.allocator.free(str); if (self.token) |token| { authorization_header = std.fmt.allocPrint(self.allocator, "Bearer {s}", .{token}) catch return APIError.OutOfMemory; opts.headers.authorization = .{ .override = authorization_header.? }; } if (uri.query) |query| { log.debug("fetch {} {s}?{s}", .{method, path, query.raw}); } else { log.debug("fetch {} {s}", .{method, path}); } const result = self.client.fetch(opts) catch return APIError.RequestFailed; const response_body = response_storage.items; log.debug("fetch result {}", .{result.status}); if (result.status == .service_unavailable) { return APIError.ServerUnavailable; } else if (result.status != .ok) { return ArtifactsFetchResult{ .arena = arena, .status = result.status }; } const parsed = json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }) catch return APIError.ParseFailed; if (parsed != json.Value.object) { return APIError.ParseFailed; } result_status = result.status; if (options.paginated) { const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return APIError.ParseFailed; if (total_pages_i64 < 0) return APIError.ParseFailed; total_pages = @intCast(total_pages_i64); const page_results = json_utils.getArray(parsed.object, "data") orelse return APIError.ParseFailed; fetch_results.appendSlice(page_results.items) catch return APIError.OutOfMemory; if (current_page >= total_pages) break; } else { result_body = parsed.object.get("data"); break; } } if (options.paginated) { result_body = json.Value{ .array = fetch_results }; } return ArtifactsFetchResult{ .status = result_status, .arena = arena, .body = result_body }; } fn handleFetchError( status: std.http.Status, Error: type, parseError: ?fn (status: std.http.Status) ?Error, ) ?Error { if (status != .ok) { if (Error != APIError) { if (parseError == null) { @compileError("`parseError` must be defined, if `Error` is not `APIError`"); } if (parseError.?(status)) |error_value| { return error_value; } } else { if (parseError != null) { @compileError("`parseError` must be null"); } } } return null; } fn fetchOptionalObject( self: *Server, Error: type, parseError: ?fn (status: std.http.Status) ?Error, Object: type, parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions, ) Error!?Object { if (@typeInfo(@TypeOf(parseObject)) != .Fn) { @compileError("`parseObject` must be a function"); } const result = try self.fetch(fetchOptions); defer result.deinit(); if (handleFetchError(result.status, Error, parseError)) |error_value| { return error_value; } if (result.status == .not_found) { return null; } if (result.status != .ok) { return APIError.RequestFailed; } if (result.body == null) { return APIError.ParseFailed; } const body = json_utils.asObject(result.body.?) orelse return APIError.ParseFailed; return @call(.auto, parseObject, .{ self, body } ++ parseObjectArgs) catch return APIError.ParseFailed; } fn fetchObject( self: *Server, Error: type, parseError: ?fn (status: std.http.Status) ?Error, Object: type, parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions ) Error!Object { const result = try self.fetchOptionalObject(Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); return result orelse return APIError.RequestFailed; } fn fetchOptionalArray( self: *Server, allocator: Allocator, Error: type, parseError: ?fn (status: std.http.Status) ?Error, Object: type, parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions ) Error!?std.ArrayList(Object) { if (@typeInfo(@TypeOf(parseObject)) != .Fn) { @compileError("`parseObject` must be a function"); } const result = try self.fetch(fetchOptions); defer result.deinit(); if (handleFetchError(result.status, Error, parseError)) |error_value| { return error_value; } if (result.status == .not_found) { return null; } if (result.status != .ok) { return APIError.RequestFailed; } if (result.body == null) { return APIError.ParseFailed; } var array = std.ArrayList(Object).init(allocator); errdefer { if (std.meta.hasFn(Object, "deinit")) { for (array.items) |*item| { _ = item; // TODO: // item.deinit(); } } array.deinit(); } const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed; for (result_data.items) |result_item| { const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed; const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return APIError.ParseFailed; array.append(parsed_item) catch return APIError.OutOfMemory; } return array; } fn fetchArray( self: *Server, allocator: Allocator, Error: type, parseError: ?fn (status: std.http.Status) ?Error, Object: type, parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions ) Error!std.ArrayList(Object) { const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); return result orelse return APIError.RequestFailed; } pub fn setURL(self: *Server, url: []const u8) !void { const url_dupe = self.allocator.dupe(u8, url); errdefer self.allocator.free(url_dupe); const uri = try std.Uri.parse(url_dupe); self.allocator.free(self.server); self.server = url_dupe; self.server_uri = uri; } pub fn setToken(self: *Server, token: ?[]const u8) !void { var new_token: ?[]u8 = null; if (token != null) { new_token = try self.allocator.dupe(u8, token.?); } if (self.token) |str| self.allocator.free(str); self.token = new_token; } pub fn getItemId(self: *Server, 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); } } const code_dupe = try self.allocator.dupe(u8, code); errdefer self.allocator.free(code_dupe); try self.item_codes.append(code_dupe); return @intCast(self.item_codes.items.len - 1); } pub fn getItemCode(self: *const Server, id: ItemId) ?[]const u8 { if (id >= self.item_codes.items.len) { return null; } 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 findCharacterPtr(self: *Server, name: []const u8) ?*Character { if (self.findCharacterIndex(name)) |index| { return &self.characters.items[index]; } return null; } // TODO: Remove this function pub fn findItem(self: *const Server, name: []const u8) ?Item { return self.items.get(name); } // TODO: Remove this function pub fn findMap(self: *const Server, position: Position) ?MapTile { return self.maps.get(position); } fn addOrUpdateItem(self: *Server, item: Item) !void { var entry = try self.items.getOrPut(item.code); if (entry.found_existing) { entry.value_ptr.deinit(); } entry.value_ptr.* = item; } fn addOrUpdateMap(self: *Server, map: MapTile) !void { var entry = try self.maps.getOrPut(map.position); if (entry.found_existing) { entry.value_ptr.deinit(self.allocator); } entry.value_ptr.* = map; } fn addOrUpdateResource(self: *Server, resource: Resource) !void { var entry = try self.resources.getOrPut(resource.code); if (entry.found_existing) { entry.value_ptr.deinit(self.allocator); } entry.value_ptr.* = resource; } fn addOrUpdateMonster(self: *Server, monster: Monster) !void { var entry = try self.monsters.getOrPut(monster.code); if (entry.found_existing) { entry.value_ptr.deinit(self.allocator); } entry.value_ptr.* = monster; } pub fn prefetch(self: *Server) !void { self.prefetched_resources = false; self.prefetched_maps = false; self.prefetched_monsters = false; self.prefetched_items = false; try self.prefetchResources(); try self.prefetchMaps(); try self.prefetchMonsters(); try self.prefetchItems(); } pub fn prefetchResources(self: *Server) !void { var resources = try self.getResources(.{}); defer resources.deinit(); self.prefetched_resources = true; } pub fn prefetchMaps(self: *Server) !void { var maps = try self.getMaps(.{}); defer maps.deinit(); self.prefetched_maps = true; } pub fn prefetchMonsters(self: *Server) !void { var monsters = try self.getMonsters(.{}); defer monsters.deinit(); self.prefetched_monsters = true; } pub fn prefetchItems(self: *Server) !void { var items = try self.getItems(.{}); defer items.deinit(); self.prefetched_items = true; } // ------------------------- Endpoints ------------------------ pub fn getServerStatus(self: *Server) !ServerStatus { return try self.fetchObject( APIError, null, ServerStatus, ServerStatus.parse, .{ self.allocator }, .{ .method = .GET, .path = "/" } ); } pub fn getCharacter(self: *Server, name: []const u8) APIError!?Character { const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name}); defer self.allocator.free(path); var maybe_character = try self.fetchOptionalObject( APIError, null, Character, Character.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (maybe_character) |*character| { errdefer character.deinit(); try self.addOrUpdateCharacter(character.*); return character.*; } else { return null; } } pub fn listMyCharacters(self: *Server) APIError!std.ArrayList(Character) { const characters = try self.fetchArray( self.allocator, APIError, null, Character, Character.parse, .{ self.allocator }, .{ .method = .GET, .path = "/my/characters" } ); errdefer characters.deinit(); for (characters.items) |character| { try self.addOrUpdateCharacter(character); } return characters; } pub fn actionFight(self: *Server, name: []const u8) FightError!FightResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name}); defer self.allocator.free(path); const result = try self.fetchObject( FightError, FightResult.parseError, FightResult, FightResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionGather(self: *Server, name: []const u8) GatherError!GatherResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name}); defer self.allocator.free(path); const result = try self.fetchObject( GatherError, GatherResult.parseError, GatherResult, GatherResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) MoveError!MoveResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y}); defer self.allocator.free(payload); const result = try self.fetchObject( MoveError, MoveResult.parseError, MoveResult, MoveResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionBankDepositGold( self: *Server, name: []const u8, quantity: u64 ) BankDepositGoldError!GoldTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit/gold", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); defer self.allocator.free(payload); const result = try self.fetchObject( BankDepositGoldError, GoldTransactionResult.parseDepositError, GoldTransactionResult, GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionBankDepositItem( self: *Server, name: []const u8, code: []const u8, quantity: u64 ) BankDepositItemError!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); const result = try self.fetchObject( BankDepositItemError, ItemTransactionResult.parseDepositError, ItemTransactionResult, ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionBankWithdrawGold( self: *Server, name: []const u8, quantity: u64 ) BankDepositGoldError!GoldTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/withdraw/gold", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); defer self.allocator.free(payload); const result = try self.fetchObject( BankWithdrawGoldError, GoldTransactionResult.parseWithdrawError, GoldTransactionResult, GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionBankWithdrawItem( self: *Server, name: []const u8, code: []const u8, quantity: u64 ) BankWithdrawItemError!ItemTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/withdraw", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); defer self.allocator.free(payload); const result = try self.fetchObject( BankWithdrawItemError, ItemTransactionResult.parseWithdrawError, ItemTransactionResult, ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionCraft( self: *Server, name: []const u8, code: []const u8, quantity: u64 ) !CraftResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/crafting", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); defer self.allocator.free(payload); const result = try self.fetchObject( CraftError, CraftResult.parseError, CraftResult, CraftResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionUnequip( self: *Server, name: []const u8, slot: EquipmentSlot ) UnequipError!UnequipResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/unequip", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\" }}", .{slot.name()}); defer self.allocator.free(payload); const result = try self.fetchObject( UnequipError, UnequipResult.parseError, UnequipResult, UnequipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn actionEquip( self: *Server, name: []const u8, slot: EquipmentSlot, code: []const u8 ) EquipError!EquipResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/equip", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\",\"code\":\"{s}\" }}", .{slot.name(), code}); defer self.allocator.free(payload); const result = try self.fetchObject( EquipError, EquipResult.parseError, EquipResult, EquipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.addOrUpdateCharacter(result.character); return result; } pub fn getBankGold(self: *Server) APIError!u64 { const result = try self.fetch(.{ .method = .GET, .path = "/my/bank/gold" }); defer result.deinit(); if (result.status != .ok) { return APIError.RequestFailed; } if (result.body == null) { return APIError.ParseFailed; } const data = json_utils.asObject(result.body.?) orelse return APIError.RequestFailed; const quantity = json_utils.getInteger(data, "quantity") orelse return APIError.ParseFailed; if (quantity < 0) return APIError.ParseFailed; return @intCast(quantity); } pub fn getBankItems(self: *Server, allocator: Allocator) APIError!std.ArrayList(ItemIdQuantity) { return self.fetchArray( allocator, APIError, null, ItemIdQuantity, ItemIdQuantity.parse, .{}, .{ .method = .GET, .path = "/my/bank/items", .paginated = true } ); } pub fn getBankItemQuantity(self: *Server, code: []const u8) APIError!?u64 { const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code}); defer self.allocator.free(query); const maybe_items = try self.fetchOptionalArray( self.allocator, APIError, null, ItemIdQuantity, ItemIdQuantity.parse, .{}, .{ .method = .GET, .path = "/my/bank/items", .query = query, .paginated = true } ); if (maybe_items == null) { return null; } const items = maybe_items.?; defer items.deinit(); const list_items = items.list.items; assert(list_items.len == 1); assert(list_items[0].id == try self.getItemId(code)); return list_items[0].quantity; } pub fn getMap(self: *Server, x: i64, y: i64) APIError!?MapTile { const position = Position.init(x, y); if (self.findMap(position)) |map| { return map; } if (self.prefetched_maps) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y}); defer self.allocator.free(path); const result = self.fetchOptionalObject( APIError, null, MapTile, MapTile.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |map| { self.addOrUpdateMap(map); } return result; } pub const MapOptions = struct { code: ?[]const u8 = null, type: ?MapContentType = null, }; pub fn getMaps(self: *Server, opts: MapOptions) APIError!std.ArrayList(MapTile) { if (self.prefetched_maps) { var found = std.ArrayList(MapTile).init(self.allocator); var mapIter = self.maps.valueIterator(); while (mapIter.next()) |map| { if (opts.type) |content_type| { if (map.content == null) continue; if (map.content.?.type != content_type) continue; } if (opts.code) |content_code| { if (map.content == null) continue; if (!std.mem.eql(u8, map.content.?.code, content_code)) continue; } try found.append(map.*); } return found; } var query = std.ArrayList(u8).init(self.allocator); defer query.deinit(); if (opts.code) |code| { try appendQueryParam(&query, "content_code", code); } if (opts.type) |map_type| { try appendQueryParam(&query, "content_type", MapContentTypeUtils.toString(map_type)); } const result = try self.fetchArray( self.allocator, APIError, null, MapTile, MapTile.parse, .{ self.allocator }, .{ .method = .GET, .path = "/maps", .paginated = true, .page_size = 100, .query = query.items } ); for (result.items) |map| { try self.addOrUpdateMap(map); } return result; } pub fn getItem(self: *Server, code: []const u8) APIError!?Item { if (self.items.get(code)) |item| { return item; } if (self.prefetched_items) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code}); defer self.allocator.free(path); const result = try self.fetchOptionalObject( APIError, null, ItemWithGE, ItemWithGE.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |item_with_ge| { try self.addOrUpdateItem(item_with_ge.item); return item_with_ge.item; } else { return null; } } pub fn getItemById(self: *Server, id: ItemId) APIError!?Item { const code = self.getItemCode(id) orelse return null; return self.getItem(code); } pub const ItemOptions = struct { craft_material: ?[]const u8 = null, craft_skill: ?Skill = null, min_level: ?u64 = null, max_level: ?u64 = null, name: ?[]const u8 = null, type: ?ItemType = null, }; pub fn getItems(self: *Server, opts: ItemOptions) APIError!std.ArrayList(Item) { if (self.prefetched_items) { var found = std.ArrayList(Item).init(self.allocator); var itemIter = self.items.valueIterator(); while (itemIter.next()) |item| { if (opts.craft_skill) |craft_skill| { if (item.craft == null) continue; if (item.craft.?.skill != craft_skill) continue; } if (opts.craft_material) |craft_material| { if (item.craft == null) continue; const recipe = item.craft.?; const craft_material_id = try self.getItemId(craft_material); const material_quantity = recipe.items.getQuantity(craft_material_id); if (material_quantity == 0) continue; } if (opts.min_level) |min_level| { if (item.level < min_level) continue; } if (opts.max_level) |max_level| { if (item.level > max_level) continue; } if (opts.type) |item_type| { if (item.type != item_type) continue; } if (opts.name) |name| { if (std.mem.indexOf(u8, item.name, name) == null) continue; } try found.append(item.*); } return found; } var str_arena = std.heap.ArenaAllocator.init(self.allocator); defer str_arena.deinit(); var query = std.ArrayList(u8).init(self.allocator); defer query.deinit(); if (opts.craft_material) |craft_material| { try appendQueryParam(&query, "craft_material", craft_material); } if (opts.min_level) |min_level| { const min_level_str = try std.fmt.allocPrint(str_arena.allocator(), "{}", .{ min_level }); try appendQueryParam(&query, "min_level", min_level_str); } if (opts.max_level) |max_level| { const max_level_str = try std.fmt.allocPrint(str_arena.allocator(), "{}", .{ max_level }); try appendQueryParam(&query, "max_level", max_level_str); } if (opts.name) |name| { try appendQueryParam(&query, "name", name); } if (opts.type) |item_type| { try appendQueryParam(&query, "type", ItemTypeUtils.toString(item_type)); } const result = try self.fetchArray( self.allocator, APIError, null, Item, Item.parse, .{ self.allocator }, .{ .method = .GET, .path = "/items", .paginated = true, .page_size = 100, .query = query.items } ); errdefer result.deinit(); for (result.items) |item| { try self.addOrUpdateItem(item); } return result; } pub fn getResource(self: *Server, code: []const u8) APIError!?Resource { if (self.resources.get(code)) |resource| { return resource; } if (self.prefetched_resources) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/resources/{s}", .{code}); defer self.allocator.free(path); const result = try self.fetchOptionalObject( APIError, null, Resource, Resource.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |resource| { try self.addOrUpdateResource(resource); } return result; } pub const ResourceOptions = struct { drop: ?[]const u8 = null, max_level: ?u64 = null, min_level: ?u64 = null, skill: ?ResourceSkill = null, }; pub fn getResources(self: *Server, opts: ResourceOptions) APIError!std.ArrayList(Resource) { if (self.prefetched_resources) { var found = std.ArrayList(Resource).init(self.allocator); var resourceIter = self.resources.valueIterator(); while (resourceIter.next()) |resource| { if (opts.min_level) |min_level| { if (resource.level < min_level) continue; } if (opts.max_level) |max_level| { if (resource.level > max_level) continue; } if (opts.drop) |drop| { const item_id = try self.getItemId(drop); if (!DropRate.doesListContain(&resource.drops, item_id)) { continue; } } try found.append(resource.*); } return found; } var str_arena = std.heap.ArenaAllocator.init(self.allocator); defer str_arena.deinit(); var query = std.ArrayList(u8).init(self.allocator); defer query.deinit(); if (opts.drop) |drop| { try appendQueryParam(&query, "drop", drop); } if (opts.min_level) |min_level| { const min_level_str = try std.fmt.allocPrint(str_arena.allocator(), "{}", .{ min_level }); try appendQueryParam(&query, "min_level", min_level_str); } if (opts.max_level) |max_level| { const max_level_str = try std.fmt.allocPrint(str_arena.allocator(), "{}", .{ max_level }); try appendQueryParam(&query, "max_level", max_level_str); } if (opts.skill) |skill| { try appendQueryParam(&query, "skill", ResourceSkillUtils.toString(skill)); } const result = try self.fetchArray( self.allocator, APIError, null, Resource, Resource.parse, .{ self.allocator }, .{ .method = .GET, .path = "/resources", .paginated = true, .query = query.items } ); errdefer result.deinit(); for (result.items) |resource| { try self.addOrUpdateResource(resource); } return result; } pub fn getMonster(self: *Server, code: []const u8) APIError!?Monster { if (self.monsters.get(code)) |monster| { return monster; } if (self.prefetched_monsters) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/monsters/{s}", .{code}); defer self.allocator.free(path); const result = try self.fetchOptionalObject( APIError, null, Monster, Monster.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |monster| { try self.addOrUpdateMonster(monster); } return result; } pub const MonsterOptions = struct { drop: ?[]const u8 = null, max_level: ?u64 = null, min_level: ?u64 = null, }; pub fn getMonsters(self: *Server, opts: MonsterOptions) APIError!std.ArrayList(Monster) { if (self.prefetched_monsters) { var found = std.ArrayList(Monster).init(self.allocator); var monsterIter = self.monsters.valueIterator(); while (monsterIter.next()) |monster| { if (opts.min_level) |min_level| { if (monster.level < min_level) continue; } if (opts.max_level) |max_level| { if (monster.level > max_level) continue; } if (opts.drop) |drop| { const item_id = try self.getItemId(drop); if (!DropRate.doesListContain(&monster.drops, item_id)) { continue; } } try found.append(monster.*); } return found; } var str_arena = std.heap.ArenaAllocator.init(self.allocator); defer str_arena.deinit(); var query = std.ArrayList(u8).init(self.allocator); defer query.deinit(); if (opts.drop) |drop| { try appendQueryParam(&query, "drop", drop); } if (opts.min_level) |min_level| { const min_level_str = try std.fmt.allocPrint(str_arena.allocator(), "{}", .{ min_level }); try appendQueryParam(&query, "min_level", min_level_str); } if (opts.max_level) |max_level| { const max_level_str = try std.fmt.allocPrint(str_arena.allocator(), "{}", .{ max_level }); try appendQueryParam(&query, "max_level", max_level_str); } const result = try self.fetchArray( self.allocator, APIError, null, Monster, Monster.parse, .{ self.allocator }, .{ .method = .GET, .path = "/monsters", .paginated = true, .query = query.items } ); for (result.items) |monster| { try self.addOrUpdateMonster(monster); } return result; }