const std = @import("std"); const json_utils = @import("json_utils.zig"); const assert = std.debug.assert; const json = std.json; const Allocator = std.mem.Allocator; const Character = @import("./schemas/character.zig"); const EnumStringUtils = @import("./enum_string_utils.zig").EnumStringUtils; const Position = @import("./position.zig"); const Store = @import("./store.zig"); const errors = @import("./errors.zig"); const FetchError = errors.FetchError; // Specification: https://api.artifactsmmo.com/docs const Server = @This(); const log = std.log.scoped(.api); allocator: Allocator, client: std.http.Client, server: []u8, server_uri: std.Uri, token: ?[]u8 = null, store: Store, prefetched: bool = false, // ------------------------- API result structs ------------------------ const BoundedSlotsArray = @import("./schemas/slot_array.zig").BoundedSlotsArray; pub const EquipmentSlot = @import("./schemas/equipment.zig").Slot; pub const ServerStatus = @import("./schemas/status.zig"); pub const Cooldown = @import("./schemas/cooldown.zig"); pub const FightResult = @import("./schemas/character_fight.zig"); pub const Skill = @import("./schemas/skill.zig").Skill; pub const GatherResult = @import("./schemas/skill_data.zig"); pub const MoveResult = @import("./schemas/character_movement.zig"); pub const GoldTransactionResult = @import("./schemas/bank_gold_transaction.zig"); pub const ItemTransactionResult = @import("./schemas/bank_item_transaction.zig"); pub const CraftResult = @import("./schemas/skill_data.zig"); pub const UnequipResult = @import("./schemas/equip_request.zig"); pub const EquipResult = @import("./schemas/equip_request.zig"); const DropRate = @import("./schemas/drop_rate.zig"); pub const AcceptTaskResult = @import("./schemas/task_data.zig"); pub const MapContent = @import("./schemas/map_content.zig"); pub const MapTile = @import("./schemas/map.zig"); pub const Item = @import("./schemas/item.zig"); pub const ItemWithGE = @import("./schemas/single_item.zig"); pub const Resource = @import("./schemas/resource.zig"); pub const Monster = @import("./schemas/monster.zig"); const ItemQuantity = @import("./schemas/item_quantity.zig"); pub const ArtifactsFetchResult = struct { arena: std.heap.ArenaAllocator, status: std.http.Status, body: ?json.Value = null, fn deinit(self: ArtifactsFetchResult) void { self.arena.deinit(); } }; // ------------------------- 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, .store = Store.init(allocator), }; } pub fn deinit(self: *Server) void { self.client.deinit(); self.allocator.free(self.server); if (self.token) |str| self.allocator.free(str); self.store.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 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); } 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) FetchError!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 FetchError.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 FetchError.RequestFailed; const response_body = response_storage.items; log.debug("fetch result {}", .{result.status}); if (result.status == .service_unavailable) { return FetchError.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 FetchError.ParseFailed; if (parsed != json.Value.object) { return FetchError.ParseFailed; } result_status = result.status; if (options.paginated) { const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return FetchError.ParseFailed; if (total_pages_i64 < 0) return FetchError.ParseFailed; total_pages = @intCast(total_pages_i64); const page_results = json_utils.getArray(parsed.object, "data") orelse return FetchError.ParseFailed; fetch_results.appendSlice(page_results.items) catch return FetchError.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 != FetchError) { 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 FetchError.RequestFailed; } if (result.body == null) { return FetchError.ParseFailed; } const body = json_utils.asObject(result.body.?) orelse return FetchError.ParseFailed; return @call(.auto, parseObject, .{ &self.store, body } ++ parseObjectArgs) catch return FetchError.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 FetchError.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 FetchError.RequestFailed; } if (result.body == null) { return FetchError.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 FetchError.ParseFailed; for (result_data.items) |result_item| { const item_obj = json_utils.asObject(result_item) orelse return FetchError.ParseFailed; const parsed_item = @call(.auto, parseObject, .{ &self.store, item_obj } ++ parseObjectArgs) catch return FetchError.ParseFailed; array.append(parsed_item) catch return FetchError.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 FetchError.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 prefetch(self: *Server) !void { self.prefetched = false; const resources = try self.getResources(.{}); defer resources.deinit(); const maps = try self.getMaps(.{}); defer maps.deinit(); const monsters = try self.getMonsters(.{}); defer monsters.deinit(); const items = try self.getItems(.{}); defer items.deinit(); self.prefetched = true; } // ------------------------- Endpoints ------------------------ pub fn getServerStatus(self: *Server) FetchError!ServerStatus { return try self.fetchObject( FetchError, null, ServerStatus, ServerStatus.parse, .{ self.allocator }, .{ .method = .GET, .path = "/" } ); } pub fn getCharacter(self: *Server, name: []const u8) FetchError!?Character { const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name}); defer self.allocator.free(path); var maybe_character = try self.fetchOptionalObject( FetchError, null, Character, Character.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (maybe_character) |*character| { errdefer character.deinit(); try self.store.putCharacter(character.*); return character.*; } else { return null; } } pub fn listMyCharacters(self: *Server) FetchError!std.ArrayList(Character) { const characters = try self.fetchArray( self.allocator, FetchError, null, Character, Character.parse, .{ self.allocator }, .{ .method = .GET, .path = "/my/characters" } ); errdefer characters.deinit(); for (characters.items) |character| { try self.store.putCharacter(character); } return characters; } pub fn actionFight(self: *Server, name: []const u8) errors.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( errors.FightError, errors.parseFightError, FightResult, FightResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); try self.store.putCharacter(result.character); return result; } pub fn actionGather(self: *Server, name: []const u8) errors.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( errors.GatherError, errors.parseGatherError, GatherResult, GatherResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); try self.store.putCharacter(result.character); return result; } pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) errors.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( errors.MoveError, errors.parseMoveError, MoveResult, MoveResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn actionBankDepositGold( self: *Server, name: []const u8, quantity: u64 ) errors.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( errors.BankDepositGoldError, errors.parseBankDepositGoldError, GoldTransactionResult, GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn actionBankDepositItem( self: *Server, name: []const u8, code: []const u8, quantity: u64 ) errors.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( errors.BankDepositItemError, errors.parseBankDepositItemError, ItemTransactionResult, ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn actionBankWithdrawGold( self: *Server, name: []const u8, quantity: u64 ) errors.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( errors.BankWithdrawGoldError, errors.parseBankWithdrawGoldError, GoldTransactionResult, GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn actionBankWithdrawItem( self: *Server, name: []const u8, code: []const u8, quantity: u64 ) errors.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( errors.BankWithdrawItemError, errors.parseBankWithdrawItemError, ItemTransactionResult, ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(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( errors.CraftError, errors.parseCraftError, CraftResult, CraftResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn actionUnequip( self: *Server, name: []const u8, slot: EquipmentSlot ) errors.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( errors.UnequipError, errors.parseUnequipError, UnequipResult, UnequipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn actionEquip( self: *Server, name: []const u8, slot: EquipmentSlot, code: []const u8 ) errors.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( errors.EquipError, errors.parseEquipError, EquipResult, EquipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); try self.store.putCharacter(result.character); return result; } pub fn getBankGold(self: *Server) FetchError!u64 { const result = try self.fetch(.{ .method = .GET, .path = "/my/bank/gold" }); defer result.deinit(); if (result.status != .ok) { return FetchError.RequestFailed; } if (result.body == null) { return FetchError.ParseFailed; } const data = json_utils.asObject(result.body.?) orelse return FetchError.RequestFailed; const quantity = json_utils.getInteger(data, "quantity") orelse return FetchError.ParseFailed; if (quantity < 0) return FetchError.ParseFailed; return @intCast(quantity); } pub fn getBankItems(self: *Server, allocator: Allocator) FetchError!std.ArrayList(ItemQuantity) { return self.fetchArray( allocator, FetchError, null, ItemQuantity, ItemQuantity.parse, .{}, .{ .method = .GET, .path = "/my/bank/items", .paginated = true } ); } pub fn getBankItemQuantity(self: *Server, code: []const u8) FetchError!?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, FetchError, null, ItemQuantity, ItemQuantity.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) FetchError!?MapTile { if (self.store.getMap(x, y)) |map| { return map; } if (self.prefetched) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y}); defer self.allocator.free(path); const result = self.fetchOptionalObject( FetchError, null, MapTile, MapTile.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |map| { self.store.putMap(map); } return result; } pub const MapOptions = struct { code: ?[]const u8 = null, type: ?MapContent.Type = null, }; pub fn getMaps(self: *Server, opts: MapOptions) FetchError!std.ArrayList(MapTile) { if (self.prefetched) { return try self.store.getMaps(opts); } 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", MapContent.TypeUtils.toString(map_type)); } const result = try self.fetchArray( self.allocator, FetchError, null, MapTile, MapTile.parse, .{ self.allocator }, .{ .method = .GET, .path = "/maps", .paginated = true, .page_size = 100, .query = query.items } ); for (result.items) |map| { try self.store.putMap(map); } return result; } pub fn getItem(self: *Server, code: []const u8) FetchError!?Item { if (self.store.getItem(code)) |item| { return item; } if (self.prefetched) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code}); defer self.allocator.free(path); const result = try self.fetchOptionalObject( FetchError, null, ItemWithGE, ItemWithGE.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |item_with_ge| { try self.store.putItem(item_with_ge.item); return item_with_ge.item; } else { return null; } } pub fn getItemById(self: *Server, id: Store.CodeId) FetchError!?Item { const code = self.store.getCode(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: ?Item.Type = null, }; pub fn getItems(self: *Server, opts: ItemOptions) FetchError!std.ArrayList(Item) { if (self.prefetched) { return try self.store.getItems(opts); } 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", Item.TypeUtils.toString(item_type)); } const result = try self.fetchArray( self.allocator, FetchError, 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.store.putItem(item); } return result; } pub fn getResource(self: *Server, code: []const u8) FetchError!?Resource { if (self.store.getResource(code)) |resource| { return resource; } if (self.prefetched) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/resources/{s}", .{code}); defer self.allocator.free(path); const result = try self.fetchOptionalObject( FetchError, null, Resource, Resource.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |resource| { try self.store.putResource(resource); } return result; } pub const ResourceOptions = struct { drop: ?[]const u8 = null, max_level: ?u64 = null, min_level: ?u64 = null, skill: ?Resource.Skill = null, }; pub fn getResources(self: *Server, opts: ResourceOptions) FetchError!std.ArrayList(Resource) { if (self.prefetched) { return self.store.getResources(opts); } 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", Resource.SkillUtils.toString(skill)); } const result = try self.fetchArray( self.allocator, FetchError, null, Resource, Resource.parse, .{ self.allocator }, .{ .method = .GET, .path = "/resources", .paginated = true, .query = query.items } ); errdefer result.deinit(); for (result.items) |resource| { try self.store.putResource(resource); } return result; } pub fn getMonster(self: *Server, code: []const u8) FetchError!?Monster { if (self.store.getMonster(code)) |monster| { return monster; } if (self.prefetched) { return null; } const path = try std.fmt.allocPrint(self.allocator, "/monsters/{s}", .{code}); defer self.allocator.free(path); const result = try self.fetchOptionalObject( FetchError, null, Monster, Monster.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); if (result) |monster| { try self.store.putMonster(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) FetchError!std.ArrayList(Monster) { if (self.prefetched) { return try self.store.getMonsters(opts); } 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, FetchError, null, Monster, Monster.parse, .{ self.allocator }, .{ .method = .GET, .path = "/monsters", .paginated = true, .query = query.items } ); for (result.items) |monster| { try self.store.putMonster(monster); } return result; } pub fn acceptTask(self: *Server, name: []const u8) errors.TaskAcceptError!AcceptTaskResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/task/new", .{name}); defer self.allocator.free(path); const result = try self.fetchObject( errors.TaskAcceptError, errors.parseTaskAcceptError, AcceptTaskResult, AcceptTaskResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); try self.store.putCharacter(result.character); return result; }