1046 lines
31 KiB
Zig
1046 lines
31 KiB
Zig
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;
|
|
}
|