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