artificer/api/server.zig

1331 lines
40 KiB
Zig

const std = @import("std");
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("./schemas/character.zig");
const EnumStringUtils = @import("./enum_string_utils.zig").EnumStringUtils;
pub const Position = @import("./position.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);
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 result structs ------------------------
pub const Slot = @import("./schemas/slot.zig");
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");
// 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 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,
.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 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, 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, 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 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) 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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.addOrUpdateCharacter(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.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(
errors.CraftError,
errors.parseCraftError,
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
) 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.addOrUpdateCharacter(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.addOrUpdateCharacter(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(ItemIdQuantity) {
return self.fetchArray(
allocator,
FetchError,
null,
ItemIdQuantity,
ItemIdQuantity.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,
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) FetchError!?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(
FetchError,
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: ?MapContent.Type = null,
};
pub fn getMaps(self: *Server, opts: MapOptions) FetchError!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", 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.addOrUpdateMap(map);
}
return result;
}
pub fn getItem(self: *Server, code: []const u8) FetchError!?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(
FetchError,
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) FetchError!?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: ?Item.Type = null,
};
pub fn getItems(self: *Server, opts: ItemOptions) FetchError!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", 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.addOrUpdateItem(item);
}
return result;
}
pub fn getResource(self: *Server, code: []const u8) FetchError!?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(
FetchError,
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: ?Resource.Skill = null,
};
pub fn getResources(self: *Server, opts: ResourceOptions) FetchError!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", 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.addOrUpdateResource(resource);
}
return result;
}
pub fn getMonster(self: *Server, code: []const u8) FetchError!?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(
FetchError,
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) FetchError!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,
FetchError,
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;
}
pub fn acceptTask(self: *Server, name: []const u8) errors.TaskAcceptError {
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,
EquipResult,
EquipResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path }
);
try self.addOrUpdateCharacter(result.character);
return result;
}