Compare commits

..

9 Commits

82 changed files with 13042 additions and 3319 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.zig-cache
zig-out
result
token

View File

@ -1,15 +1,15 @@
const std = @import("std");
const assert = std.debug.assert;
pub fn EnumStringUtils(TargetEnum: anytype, str_to_tag_mapping: anytype) type {
// TODO: use `EnumFieldStruct` for `str_to_tag_mapping`
pub fn EnumStringUtils(comptime TargetEnum: type, str_to_tag_mapping: anytype) type {
if (str_to_tag_mapping.len != @typeInfo(TargetEnum).Enum.fields.len) {
@compileLog("Mapping is not exhaustive");
}
const EnumMapping = std.StaticStringMap(TargetEnum).initComptime(str_to_tag_mapping);
return struct {
pub fn fromString(str: []const u8) ?TargetEnum {
const EnumMapping = std.StaticStringMap(TargetEnum).initComptime(str_to_tag_mapping);
return EnumMapping.get(str);
}

View File

@ -5,10 +5,7 @@ const ErrorDefinition = struct {
code: ?u10,
fn init(name: [:0]const u8, code: ?u10) ErrorDefinition {
return ErrorDefinition{
.name = name,
.code = code
};
return ErrorDefinition{ .name = name, .code = code };
}
};
@ -34,38 +31,85 @@ fn ErrorDefinitionList(errors: []const ErrorDefinition) type {
};
}
const ServerUnavailable = ErrorDefinition.init("ServerUnavailable", 503);
const RequestFailed = ErrorDefinition.init("RequestFailed", null);
const ParseFailed = ErrorDefinition.init("ParseFailed", null);
const OutOfMemory = ErrorDefinition.init("OutOfMemory", null);
// zig fmt: off
const MapNotFound = ErrorDefinition.init("MapNotFound", 404);
const ItemNotFound = ErrorDefinition.init("ItemNotFound", 404);
const RecipeNotFound = ErrorDefinition.init("RecipeNotFound", 404);
pub const NotAuthenticated = ErrorDefinition.init("NotAuthenticated", 403);
pub const ServerUnavailable = ErrorDefinition.init("ServerUnavailable", 503);
pub const RequestFailed = ErrorDefinition.init("RequestFailed", null);
pub const ParseFailed = ErrorDefinition.init("ParseFailed", null);
pub const OutOfMemory = ErrorDefinition.init("OutOfMemory", null);
const BankIsBusy = ErrorDefinition.init("BankIsBusy", 461);
const NotEnoughItems = ErrorDefinition.init("NotEnoughItems", 478);
const SlotIsFull = ErrorDefinition.init("SlotIsFull", 485);
const CharacterIsBusy = ErrorDefinition.init("CharacterIsBusy", 486);
const AlreadyHasTask = ErrorDefinition.init("AlreadyHasTask", 486);
const HasNoTask = ErrorDefinition.init("HasNoTask", 487);
const TaskNotCompleted = ErrorDefinition.init("TaskNotCompleted", 488);
// List of error codes gotten from https://docs.artifactsmmo.com/api_guide/response_codes
// General
pub const InvalidPayload = ErrorDefinition.init("InvalidPayload", 422);
pub const TooManyRequests = ErrorDefinition.init("TooManyRequests", 429);
pub const NotFound = ErrorDefinition.init("NotFound", 404);
pub const FatalError = ErrorDefinition.init("FatalError", 500);
const CharacterAtDestination = ErrorDefinition.init("CharacterAtDestination", 490);
const SlotIsEmpty = ErrorDefinition.init("SlotIsEmpty", 491);
const NotEnoughGold = ErrorDefinition.init("NotEnoughGold", 492);
const NotEnoughSkill = ErrorDefinition.init("NotEnoughSkill", 493);
const CharacterIsFull = ErrorDefinition.init("CharacterIsFull", 497);
const CharacterNotFound = ErrorDefinition.init("CharacterNotFound", 498);
const CharacterInCooldown = ErrorDefinition.init("CharacterInCooldown", 499);
// Account Error Codes
const TokenInvalid = ErrorDefinition.init("TokenInvalid", 452);
const TokenExpired = ErrorDefinition.init("TokenExpired", 453);
const TokenMissing = ErrorDefinition.init("TokenMissing", 454);
const TokenGenerationFail = ErrorDefinition.init("TokenGenerationFail", 455);
const UsernameAlreadyUsed = ErrorDefinition.init("UsernameAlreadyUsed", 456);
const EmailAlreadyUsed = ErrorDefinition.init("EmailAlreadyUsed", 457);
const SamePassword = ErrorDefinition.init("SamePassword", 458);
const CurrentPasswordInvalid = ErrorDefinition.init("CurrentPasswordInvalid", 459);
const BankNotFound = ErrorDefinition.init("BankNotFound", 598);
const MonsterNotFound = ErrorDefinition.init("MonsterNotFound", 598);
const ResourceNotFound = ErrorDefinition.init("ResourceNotFound", 598);
const WorkshopNotFound = ErrorDefinition.init("WorkshopNotFound", 598);
const TaskMasterNotFound = ErrorDefinition.init("TaskMasterNotFound", 598);
// Character Error Codes
const CharacterNotEnoughHp = ErrorDefinition.init("CharacterNotEnoughHp", 483);
const CharacterMaximumUtilitesEquiped = ErrorDefinition.init("CharacterMaximumUtilitesEquiped", 484);
const CharacterItemAlreadyEquiped = ErrorDefinition.init("CharacterItemAlreadyEquiped", 485);
const CharacterLocked = ErrorDefinition.init("CharacterLocked", 486);
const CharacterNotThisTask = ErrorDefinition.init("CharacterNotThisTask", 474);
const CharacterTooManyItemsTask = ErrorDefinition.init("CharacterTooManyItemsTask", 475);
const CharacterNoTask = ErrorDefinition.init("CharacterNoTask", 487);
const CharacterTaskNotCompleted = ErrorDefinition.init("CharacterTaskNotCompleted", 488);
const CharacterAlreadyTask = ErrorDefinition.init("CharacterAlreadyTask", 489);
const CharacterAlreadyMap = ErrorDefinition.init("CharacterAlreadyMap", 490);
const CharacterSlotEquipmentError = ErrorDefinition.init("CharacterSlotEquipmentError", 491);
const CharacterGoldInsufficient = ErrorDefinition.init("CharacterGoldInsufficient", 492);
const CharacterNotSkillLevelRequired = ErrorDefinition.init("CharacterNotSkillLevelRequired", 493);
const CharacterNameAlreadyUsed = ErrorDefinition.init("CharacterNameAlreadyUsed", 494);
const MaxCharactersReached = ErrorDefinition.init("MaxCharactersReached", 495);
const CharacterNotLevelRequired = ErrorDefinition.init("CharacterNotLevelRequired", 496);
const CharacterInventoryFull = ErrorDefinition.init("CharacterInventoryFull", 497);
const CharacterNotFound = ErrorDefinition.init("CharacterNotFound", 498);
const CharacterInCooldown = ErrorDefinition.init("CharacterInCooldown", 499);
// Item Error Codes
const ItemInsufficientQuantity = ErrorDefinition.init("ItemInsufficientQuantity", 471);
const ItemInvalidEquipment = ErrorDefinition.init("ItemInvalidEquipment", 472);
const ItemRecyclingInvalidItem = ErrorDefinition.init("ItemRecyclingInvalidItem", 473);
const ItemInvalidConsumable = ErrorDefinition.init("ItemInvalidConsumable", 476);
const MissingItem = ErrorDefinition.init("MissingItem", 478);
// Grand Exchange Error Codes
const GeMaxQuantity = ErrorDefinition.init("GeMaxQuantity", 479);
const GeNotInStock = ErrorDefinition.init("GeNotInStock", 480);
const GeNotThePrice = ErrorDefinition.init("GeNotThePrice", 482);
const GeTransactionInProgress = ErrorDefinition.init("GeTransactionInProgress", 436);
const GeNoOrders = ErrorDefinition.init("GeNoOrders", 431);
const GeMaxOrders = ErrorDefinition.init("GeMaxOrders", 433);
const GeTooManyItems = ErrorDefinition.init("GeTooManyItems", 434);
const GeSameAccount = ErrorDefinition.init("GeSameAccount", 435);
const GeInvalidItem = ErrorDefinition.init("GeInvalidItem", 437);
const GeNotYourOrder = ErrorDefinition.init("GeNotYourOrder", 438);
// Bank Error Codes
const BankInsufficientGold = ErrorDefinition.init("BankInsufficientGold", 460);
const BankTransactionInProgress = ErrorDefinition.init("BankTransactionInProgress", 461);
const BankFull = ErrorDefinition.init("BankFull", 462);
// Maps Error Codes
const MapNotFound = ErrorDefinition.init("MapNotFound", 597);
const MapContentNotFound = ErrorDefinition.init("MapContentNotFound", 598);
pub const FetchError = ErrorDefinitionList(&[_]ErrorDefinition{
NotAuthenticated,
TooManyRequests,
FatalError,
InvalidPayload,
ServerUnavailable,
RequestFailed,
ParseFailed,
@ -74,8 +118,8 @@ pub const FetchError = ErrorDefinitionList(&[_]ErrorDefinition{
const MoveErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
MapNotFound,
CharacterIsBusy,
CharacterAtDestination,
CharacterLocked,
CharacterAlreadyMap,
CharacterNotFound,
CharacterInCooldown
});
@ -83,91 +127,91 @@ pub const MoveError = FetchError || MoveErrorDef.ErrorSet;
pub const parseMoveError = MoveErrorDef.parse;
const FightErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterIsBusy,
CharacterIsFull,
CharacterLocked,
CharacterInventoryFull,
CharacterNotFound,
CharacterInCooldown,
MonsterNotFound,
MapContentNotFound,
});
pub const FightError = FetchError || FightErrorDef.ErrorSet;
pub const parseFightError = FightErrorDef.parse;
const GatherErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterIsBusy,
NotEnoughSkill,
CharacterIsFull,
CharacterLocked,
CharacterNotSkillLevelRequired,
CharacterInventoryFull,
CharacterNotFound,
CharacterInCooldown,
ResourceNotFound
MapContentNotFound
});
pub const GatherError = FetchError || GatherErrorDef.ErrorSet;
pub const parseGatherError = GatherErrorDef.parse;
const BankDepositItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
ItemNotFound,
BankIsBusy,
NotEnoughItems,
CharacterIsBusy,
NotFound,
BankTransactionInProgress,
MissingItem,
CharacterLocked,
CharacterNotFound,
CharacterInCooldown,
BankNotFound
MapContentNotFound
});
pub const BankDepositItemError = FetchError || BankDepositItemErrorDef.ErrorSet;
pub const parseBankDepositItemError = BankDepositItemErrorDef.parse;
const BankDepositGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
BankIsBusy,
NotEnoughGold,
CharacterIsBusy,
BankTransactionInProgress,
BankInsufficientGold,
CharacterLocked,
CharacterNotFound,
CharacterInCooldown,
BankNotFound
MapContentNotFound
});
pub const BankDepositGoldError = FetchError || BankDepositGoldErrorDef.ErrorSet;
pub const parseBankDepositGoldError = BankDepositGoldErrorDef.parse;
const BankWithdrawGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
BankIsBusy,
NotEnoughGold,
CharacterIsBusy,
BankTransactionInProgress,
BankInsufficientGold,
CharacterLocked,
CharacterNotFound,
CharacterInCooldown,
BankNotFound
MapContentNotFound
});
pub const BankWithdrawGoldError = FetchError || BankWithdrawGoldErrorDef.ErrorSet;
pub const parseBankWithdrawGoldError = BankWithdrawGoldErrorDef.parse;
const BankWithdrawItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
ItemNotFound,
BankIsBusy,
NotEnoughItems,
CharacterIsBusy,
CharacterIsFull,
NotFound,
BankTransactionInProgress,
MissingItem,
CharacterLocked,
CharacterInventoryFull,
CharacterNotFound,
CharacterInCooldown,
BankNotFound
MapContentNotFound
});
pub const BankWithdrawItemError = FetchError || BankWithdrawItemErrorDef.ErrorSet;
pub const parseBankWithdrawItemError = BankWithdrawItemErrorDef.parse;
const CraftErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
RecipeNotFound,
NotEnoughItems,
CharacterIsBusy,
NotEnoughSkill,
CharacterIsFull,
NotFound,
MissingItem,
CharacterLocked,
CharacterNotSkillLevelRequired,
CharacterInventoryFull,
CharacterNotFound,
CharacterInCooldown,
WorkshopNotFound
MapContentNotFound
});
pub const CraftError = FetchError || CraftErrorDef.ErrorSet;
pub const parseCraftError = CraftErrorDef.parse;
const UnequipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
ItemNotFound, // TODO: Can this really occur? maybe a bug in docs
CharacterIsBusy,
SlotIsEmpty,
CharacterIsFull,
NotFound,
CharacterLocked,
CharacterSlotEquipmentError,
CharacterInventoryFull,
CharacterNotFound,
CharacterInCooldown,
});
@ -175,10 +219,11 @@ pub const UnequipError = FetchError || UnequipErrorDef.ErrorSet;
pub const parseUnequipError = UnequipErrorDef.parse;
const EquipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
ItemNotFound,
SlotIsFull,
CharacterIsBusy,
NotEnoughSkill,
NotFound,
CharacterItemAlreadyEquiped,
CharacterLocked,
CharacterSlotEquipmentError,
CharacterNotSkillLevelRequired,
CharacterNotFound,
CharacterInCooldown,
});
@ -186,23 +231,35 @@ pub const EquipError = FetchError || EquipErrorDef.ErrorSet;
pub const parseEquipError = EquipErrorDef.parse;
const AcceptTaskErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterIsBusy,
AlreadyHasTask,
CharacterLocked,
CharacterNotFound,
CharacterInCooldown,
TaskMasterNotFound
MapContentNotFound
});
pub const AcceptTaskError = FetchError || AcceptTaskErrorDef.ErrorSet;
pub const parseAcceptTaskError = AcceptTaskErrorDef.parse;
const TaskCompleteErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterIsBusy,
HasNoTask,
TaskNotCompleted,
CharacterIsFull,
CharacterLocked,
CharacterNoTask,
CharacterTaskNotCompleted,
CharacterInventoryFull,
CharacterNotFound,
CharacterInCooldown,
TaskMasterNotFound
MapContentNotFound
});
pub const TaskCompleteError = FetchError || TaskCompleteErrorDef.ErrorSet;
pub const parseTaskCompleteError = TaskCompleteErrorDef.parse;
const CreateCharacterErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterNameAlreadyUsed,
MaxCharactersReached
});
pub const CreateCharacterError = FetchError || CreateCharacterErrorDef.ErrorSet;
pub const parseCreateCharacterError = CreateCharacterErrorDef.parse;
const DeleteCharacterErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterNotFound,
});
pub const DeleteCharacterError = FetchError || DeleteCharacterErrorDef.ErrorSet;
pub const parseDeleteCharacterError = DeleteCharacterErrorDef.parse;

View File

@ -19,6 +19,15 @@ pub fn getIntegerRequired(object: json.ObjectMap, name: []const u8) !i64 {
return getInteger(object, name) orelse return error.MissingProperty;
}
pub fn getPositiveIntegerRequired(object: json.ObjectMap, name: []const u8) !u64 {
const value = try getIntegerRequired(object, name);
if (value < 0) {
return error.InvalidInteger;
}
return @intCast(value);
}
pub fn asObject(value: json.Value) ?json.ObjectMap {
if (value != json.Value.object) {
return null;
@ -70,11 +79,6 @@ pub fn getStringRequired(object: json.ObjectMap, name: []const u8) ![]const u8 {
return getString(object, name) orelse return error.MissingProperty;
}
pub fn dupeString(allocator: Allocator, object: json.ObjectMap, name: []const u8) !?[]u8 {
const str = getString(object, name) orelse return null;
return try allocator.dupe(u8, str);
}
pub fn dupeStringRequired(allocator: Allocator, object: json.ObjectMap, name: []const u8) ![]u8 {
return (try dupeString(allocator, object, name)) orelse return error.MissingProperty;
pub fn getArrayRequired(object: json.ObjectMap, name: []const u8) !json.Array {
return getArray(object, name) orelse return error.MissingProperty;
}

View File

@ -1,28 +0,0 @@
const std = @import("std");
const Position = @This();
x: i64,
y: i64,
pub fn init(x: i64, y: i64) Position {
return Position{
.x = x,
.y = y
};
}
pub fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
}
pub fn format(
self: Position,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{{ {}, {} }}", .{self.x, self.y});
}

83
api/ratelimit.zig Normal file
View File

@ -0,0 +1,83 @@
// zig fmt: off
const std = @import("std");
const RateLimit = @This();
const assert = std.debug.assert;
pub const Category = enum { account_creation, token, data, actions };
pub const Timespan = struct {
counter: u32 = 0,
limit: u32,
timer_ms: u64 = 0,
};
pub const CategoryArray = std.EnumArray(Category, RateLimit);
seconds: ?Timespan = null,
minutes: ?Timespan = null,
hours: ?Timespan = null,
last_update_at_ms: i64 = 0,
pub fn init(now_ms: i64, limit_per_hour: ?u32, limit_per_minute: ?u32, limit_per_second: ?u32) RateLimit {
var seconds: ?Timespan = null;
if (limit_per_second) |limit| {
seconds = Timespan{ .limit = limit };
}
var minutes: ?Timespan = null;
if (limit_per_minute) |limit| {
minutes = Timespan{ .limit = limit };
}
var hours: ?Timespan = null;
if (limit_per_hour) |limit| {
hours = Timespan{ .limit = limit };
}
return RateLimit{
.seconds = seconds,
.minutes = minutes,
.hours = hours,
.last_update_at_ms = now_ms
};
}
pub fn update_timers(self: *RateLimit, now_ms: i64) void {
const time_passed_ms = now_ms - self.last_update_at_ms;
assert(time_passed_ms >= 0);
inline for (.{
.{ &self.seconds, std.time.ms_per_s },
.{ &self.minutes, std.time.ms_per_min },
.{ &self.hours, std.time.ms_per_hour },
}) |tuple| {
const maybe_timespan = tuple[0];
const timespan_size = tuple[1];
if (maybe_timespan.*) |*timespan| {
timespan.timer_ms += @intCast(time_passed_ms);
const ms_per_request = @divFloor(timespan_size, timespan.limit);
const requests_passed: u32 = @intCast(@divFloor(timespan.timer_ms, ms_per_request));
timespan.counter -= @min(timespan.counter, requests_passed);
timespan.timer_ms = @mod(timespan.timer_ms, ms_per_request);
}
}
self.last_update_at_ms = now_ms;
}
pub fn increment_counters(self: *RateLimit) void {
inline for (.{
&self.hours,
&self.minutes,
&self.seconds,
}) |maybe_timespan| {
if (maybe_timespan.*) |*timespan| {
timespan.counter += 1;
}
}
}

View File

@ -1,17 +1,36 @@
const std = @import("std");
pub const max_auth_token_size = 256;
pub const AuthToken = std.BoundedArray(u8, max_auth_token_size);
pub const images_url = "https://artifactsmmo.com";
pub const images_uri = std.Uri.parse(images_url) catch @compileError("Images server URL is invalid");
// Specification URL: https://api.artifactsmmo.com/docs
pub const api_url = "https://api.artifactsmmo.com";
pub const docs_url = api_url ++ "/openapi.json";
pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime;
pub const Server = @import("server.zig");
pub const RateLimit = @import("./ratelimit.zig");
pub const Store = @import("store.zig");
pub const Character = @import("./schemas/character.zig");
pub const ServerStatus = @import("./schemas/status.zig");
pub const Item = @import("./schemas/item.zig");
pub const Status = @import("./schemas/status.zig");
pub const Position = @import("./schemas/position.zig");
pub const Map = @import("./schemas/map.zig");
pub const Position = @import("position.zig");
pub const BoundedSlotsArray = @import("schemas/slot_array.zig").BoundedSlotsArray;
pub const Slot = Server.Slot;
pub const CodeId = Store.CodeId;
pub const ItemQuantity = @import("./schemas/item_quantity.zig");
pub const Character = @import("./schemas/character.zig");
pub const Equipment = @import("./schemas/equipment.zig");
pub const Craft = @import("./schemas/craft.zig");
pub const Resource = @import("./schemas/resource.zig");
pub const MoveResult = @import("./schemas/move_result.zig");
pub const SimpleItem = @import("./schemas/simple_item.zig");
pub const EquipResult = @import("./schemas/equip_result.zig");
pub const UnequipResult = EquipResult;
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
pub const GatherResult = SkillUsageResult;
pub const CraftResult = SkillUsageResult;
pub const Cooldown = @import("./schemas/cooldown.zig");
const errors = @import("errors.zig");
pub const FetchError = errors.FetchError;
@ -23,3 +42,5 @@ pub const BankDepositItemError = errors.BankDepositItemError;
pub const BankWithdrawItemError = errors.BankWithdrawItemError;
pub const CraftError = errors.CraftError;
pub const AcceptTaskError = errors.AcceptTaskError;
pub const EquipError = errors.EquipError;
pub const UnequipError = errors.UnequipError;

View File

@ -1,23 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const BankGoldTransaction = @This();
cooldown: Cooldown,
character: Character,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !BankGoldTransaction {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return BankGoldTransaction{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(store, character, allocator)
};
}

View File

@ -1,23 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const BankItemTransaction = @This();
cooldown: Cooldown,
character: Character,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !BankItemTransaction {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return BankItemTransaction{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(store, character, allocator)
};
}

View File

@ -1,117 +1,162 @@
// zig fmt: off
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Store = @import("../store.zig");
const Position = @import("../position.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const Allocator = std.mem.Allocator;
const json = std.json;
const assert = std.debug.assert;
const SkillStats = @import("./skill_stats.zig");
const CombatStats = @import("./combat_stats.zig");
const Equipment = @import("./equipment.zig");
const json_utils = @import("../json_utils.zig");
pub const Equipment = @import("./equipment.zig");
const Task = @import("./task.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
const Inventory = BoundedSlotsArray(20);
const SimpleItem = @import("./simple_item.zig");
const Position = @import("./position.zig");
const Character = @This();
const TaskMasterTask = struct {
target_id: Store.CodeId,
type: Task.Type,
progress: u64,
total: u64,
pub const Skin = enum {
men1,
men2,
men3,
women1,
women2,
women3,
fn parse(store: *Store, obj: json.ObjectMap) !TaskMasterTask {
const task_target = try json_utils.getStringRequired(obj, "task");
const task_type = try json_utils.getStringRequired(obj, "task_type");
const Utils = EnumStringUtils(Skin, .{
.{ "men1" , Skin.men1 },
.{ "men2" , Skin.men2 },
.{ "men3" , Skin.men3 },
.{ "women1", Skin.women1 },
.{ "women2", Skin.women2 },
.{ "women3", Skin.women3 },
});
const progress = try json_utils.getIntegerRequired(obj, "task_progress");
if (progress < 0) {
return error.InvalidTaskProgress;
}
pub const fromString = Utils.fromString;
pub const toString = Utils.toString;
};
const total = try json_utils.getIntegerRequired(obj, "task_total");
if (total < 0) {
return error.InvalidTaskTotal;
}
pub const Skill = enum {
combat,
fishing,
weaponcrafting,
gearcrafting,
jewelrycrafting,
cooking,
woodcutting,
mining,
alchemy,
return TaskMasterTask{
.target_id = try store.getCodeId(task_target),
.type = Task.TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
.total = @intCast(total),
.progress = @intCast(progress),
const Utils = EnumStringUtils(Skill, .{
.{ "combat" , Skill.combat },
.{ "fishing" , Skill.fishing },
.{ "weaponcrafting" , Skill.weaponcrafting },
.{ "gearcrafting" , Skill.gearcrafting },
.{ "jewelrycrafting", Skill.jewelrycrafting },
.{ "cooking" , Skill.cooking },
.{ "woodcutting" , Skill.woodcutting },
.{ "mining" , Skill.mining },
.{ "alchemy" , Skill.alchemy },
});
pub const toString = Utils.toString;
pub const fromString = Utils.fromString;
};
pub const SkillStats = struct {
level: u64 = 0,
xp: u64 = 0,
max_xp: u64 = 0,
pub fn parse(object: std.json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
return SkillStats{
.level = try json_utils.getPositiveIntegerRequired(object, level),
.xp = try json_utils.getPositiveIntegerRequired(object, xp),
.max_xp = try json_utils.getPositiveIntegerRequired(object, max_xp),
};
}
};
allocator: Allocator,
pub const Element = enum {
water,
fire,
earth,
air,
};
name: []u8,
skin: []u8,
account: ?[]u8,
gold: i64,
hp: i64,
haste: i64,
position: Position,
cooldown_expiration: f64,
pub const ElementalStats = struct {
attack: i64,
damage: i64,
resistance: i64,
combat: SkillStats,
mining: SkillStats,
woodcutting: SkillStats,
fishing: SkillStats,
weaponcrafting: SkillStats,
gearcrafting: SkillStats,
jewelrycrafting: SkillStats,
cooking: SkillStats,
pub fn parse(object: std.json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !ElementalStats {
return ElementalStats{
.attack = try json_utils.getIntegerRequired(object, attack),
.damage = try json_utils.getIntegerRequired(object, damage),
.resistance = try json_utils.getIntegerRequired(object, resistance),
};
}
};
water: CombatStats,
fire: CombatStats,
earth: CombatStats,
air: CombatStats,
const TaskMasterTask = struct {
target: Store.Id,
type: Task.Type,
progress: u64,
total: u64,
fn parse(store: *Store, obj: std.json.ObjectMap) !?TaskMasterTask {
const task_target = try json_utils.getStringRequired(obj, "task");
if (task_target.len == 0) {
return null;
}
const task_type = try json_utils.getStringRequired(obj, "task_type");
const progress = try json_utils.getPositiveIntegerRequired(obj, "task_progress");
const total = try json_utils.getPositiveIntegerRequired(obj, "task_total");
return TaskMasterTask{
.target = try store.tasks.getOrReserveId(task_target),
.type = Task.Type.fromString(task_type) orelse return error.InvalidTaskType,
.total = total,
.progress = progress,
};
}
};
pub const ElementalStatsArray = std.EnumArray(Element, ElementalStats);
pub const Skills = std.EnumArray(Skill, SkillStats);
pub const max_name_size = 12;
pub const Name = std.BoundedArray(u8, max_name_size);
pub const max_account_size = 32;
pub const Account = std.BoundedArray(u8, max_account_size);
pub const Inventory = SimpleItem.BoundedArray(20);
name: Name,
account: Account,
skin: Skin,
gold: u64,
skills: Skills,
elemental_stats: ElementalStatsArray,
equipment: Equipment,
task: ?TaskMasterTask,
inventory_max_items: u64,
inventory: Inventory,
position: Position,
cooldown_expiration: ?f64,
task: ?TaskMasterTask,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Character {
const inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty;
const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") orelse return error.MissingProperty;
const x = try json_utils.getIntegerRequired(obj, "x");
const y = try json_utils.getIntegerRequired(obj, "y");
const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty;
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
const name = try json_utils.getStringRequired(obj, "name");
if (name.len == 0) {
return error.InvalidName;
}
const inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty;
if (inventory_max_items < 0) {
return error.InvalidInventoryMaxItems;
}
var task: ?TaskMasterTask = null;
const task_target = try json_utils.getStringRequired(obj, "task");
if (task_target.len > 0) {
task = try TaskMasterTask.parse(store, obj);
}
return Character{
.allocator = allocator,
.account = try json_utils.dupeString(allocator, obj, "account"),
.name = name,
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
.gold = try json_utils.getIntegerRequired(obj, "gold"),
.hp = try json_utils.getIntegerRequired(obj, "hp"),
.haste = try json_utils.getIntegerRequired(obj, "haste"),
.position = Position.init(x, y),
.cooldown_expiration = parseDateTime(cooldown_expiration) orelse return error.InvalidDateTime,
const inventory = try json_utils.getArrayRequired(obj, "inventory");
const account = try json_utils.getStringRequired(obj, "name");
const skin = try json_utils.getStringRequired(obj, "skin");
const gold = try json_utils.getPositiveIntegerRequired(obj, "gold");
const inventory_max_items = try json_utils.getPositiveIntegerRequired(obj, "inventory_max_items");
const skill_stats = Skills.init(.{
.combat = try SkillStats.parse(obj, "level", "xp", "max_xp"),
.mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"),
.woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"),
@ -120,43 +165,40 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Characte
.gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"),
.jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"),
.cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"),
.alchemy = try SkillStats.parse(obj, "alchemy_level", "alchemy_xp", "alchemy_max_xp"),
});
.water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"),
.fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"),
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
const elemental_stats = ElementalStatsArray.init(.{
.water = try ElementalStats.parse(obj, "attack_water", "dmg_water", "res_water"),
.fire = try ElementalStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"),
.earth = try ElementalStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
.air = try ElementalStats.parse(obj, "attack_air", "dmg_air", "res_air"),
});
const x = try json_utils.getIntegerRequired(obj, "x");
const y = try json_utils.getIntegerRequired(obj, "y");
var cooldown_expiration: ?f64 = null;
if (json_utils.getString(obj, "cooldown_expiration")) |date_time| {
cooldown_expiration = parseDateTime(date_time) orelse return error.FailedToParseCooldownExpiration;
}
return Character{
.name = try Name.fromSlice(name),
.account = try Account.fromSlice(account),
.skin = Skin.fromString(skin) orelse return error.SkinNotFound,
.gold = gold,
.skills = skill_stats,
.elemental_stats = elemental_stats,
.equipment = try Equipment.parse(store, obj),
.inventory_max_items = @intCast(inventory_max_items),
.task = try TaskMasterTask.parse(store, obj),
.inventory_max_items = inventory_max_items,
.inventory = try Inventory.parse(store, inventory),
.task = task
.position = Position.init(x, y),
.cooldown_expiration = cooldown_expiration
};
}
pub fn deinit(self: *Character) void {
if (self.account) |str| self.allocator.free(str);
self.allocator.free(self.name);
self.allocator.free(self.skin);
}
pub fn getItemCount(self: *const Character) u64 {
return self.inventory.totalQuantity();
}
pub fn format(
self: Character,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{s}{{ .name = \"{s}\", .position = {} ... }}", .{
@typeName(Character),
self.name,
self.position
});
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return store.characters.appendOrUpdate(try parse(store, obj));
}

View File

@ -1,27 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Fight = @import("./fight.zig");
const Character = @import("./character.zig");
const CharacterFight = @This();
cooldown: Cooldown,
fight: Fight,
character: Character,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !CharacterFight {
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 CharacterFight{
.cooldown = try Cooldown.parse(cooldown),
.fight = try Fight.parse(store, fight),
.character = try Character.parse(store, character, allocator)
};
}

View File

@ -1,23 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const CharacterMovement = @This();
cooldown: Cooldown,
character: Character,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !CharacterMovement {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return CharacterMovement{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(store, character, allocator)
};
}

View File

@ -1,26 +1,12 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const json = std.json;
const Cooldown = @This();
const ReasonUtils = EnumStringUtils(Reason, .{
.{ "movement" , Reason.movement },
.{ "fight" , Reason.fight },
.{ "crafting" , Reason.crafting },
.{ "gathering" , Reason.gathering },
.{ "buy_ge" , Reason.buy_ge },
.{ "sell_ge" , Reason.sell_ge },
.{ "delete_item" , Reason.delete_item },
.{ "deposit_bank" , Reason.deposit_bank },
.{ "withdraw_bank", Reason.withdraw_bank },
.{ "equip" , Reason.equip },
.{ "unequip" , Reason.unequip },
.{ "task" , Reason.task },
.{ "recycling" , Reason.recycling },
});
pub const Reason = enum {
movement,
fight,
@ -28,26 +14,61 @@ pub const Reason = enum {
gathering,
buy_ge,
sell_ge,
cancel_ge,
delete_item,
deposit_bank,
withdraw_bank,
deposit,
withdraw,
deposit_gold,
withdraw_gold,
equip,
unequip,
task,
christmas_exchange,
recycling,
rest,
use,
buy_bank_expansion,
const parse = ReasonUtils.fromString;
const Utils = EnumStringUtils(Reason, .{
.{ "movement" , Reason.movement },
.{ "fight" , Reason.fight },
.{ "crafting" , Reason.crafting },
.{ "gathering" , Reason.gathering },
.{ "buy_ge" , Reason.buy_ge },
.{ "sell_ge" , Reason.sell_ge },
.{ "cancel_ge" , Reason.cancel_ge },
.{ "delete_item" , Reason.delete_item },
.{ "deposit" , Reason.deposit },
.{ "withdraw" , Reason.withdraw },
.{ "deposit_gold" , Reason.deposit_gold },
.{ "withdraw_gold" , Reason.withdraw_gold },
.{ "equip" , Reason.equip },
.{ "unequip" , Reason.unequip },
.{ "task" , Reason.task },
.{ "christmas_exchange", Reason.christmas_exchange },
.{ "recycling" , Reason.recycling },
.{ "rest" , Reason.rest },
.{ "use" , Reason.use },
.{ "buy_bank_expansion", Reason.buy_bank_expansion },
});
pub const fromString = Utils.fromString;
pub const toString = Utils.toString;
};
started_at: f64,
expiration: f64,
reason: Reason,
pub fn parse(obj: json.ObjectMap) !Cooldown {
const reason = try json_utils.getStringRequired(obj, "reason");
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Cooldown {
_ = store;
const started_at = try json_utils.getStringRequired(obj, "started_at");
const expiration = try json_utils.getStringRequired(obj, "expiration");
const reason = try json_utils.getStringRequired(obj, "reason");
return Cooldown{
.expiration = parseDateTime(expiration) orelse return error.InvalidDateTime,
.reason = Reason.parse(reason) orelse return error.UnknownReason
.started_at = parseDateTime(started_at) orelse return error.InvalidStartedAt,
.expiration = parseDateTime(expiration) orelse return error.InvalidExpiration,
.reason = Reason.fromString(reason) orelse return error.InvalidReason
};
}

View File

@ -1,22 +1,57 @@
const std = @import("std");
// zig fmt: off
const Store = @import("../store.zig");
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
const json = std.json;
const Skill = @import("./skill.zig").Skill;
const SkillUtils = @import("./skill.zig").SkillUtils;
const Items = BoundedSlotsArray(8);
const SimpleItem = @import("./simple_item.zig");
const Character = @import("./character.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Craft = @This();
pub const max_items = 8;
pub const Items = SimpleItem.BoundedArray(max_items);
pub const Skill = enum {
weaponcrafting,
gearcrafting,
jewelrycrafting,
cooking,
woodcutting,
mining,
alchemy,
const Utils = EnumStringUtils(Skill, .{
.{ "weaponcrafting" , Skill.weaponcrafting },
.{ "gearcrafting" , Skill.gearcrafting },
.{ "jewelrycrafting", Skill.jewelrycrafting },
.{ "cooking" , Skill.cooking },
.{ "woodcutting" , Skill.woodcutting },
.{ "mining" , Skill.mining },
.{ "alchemy" , Skill.alchemy },
});
pub const toString = Utils.toString;
pub const fromString = Utils.fromString;
pub fn toCharacterSkill(self: Skill) Character.Skill {
return switch (self) {
.weaponcrafting => Character.Skill.weaponcrafting,
.gearcrafting => Character.Skill.gearcrafting,
.jewelrycrafting => Character.Skill.jewelrycrafting,
.cooking => Character.Skill.cooking,
.woodcutting => Character.Skill.woodcutting,
.mining => Character.Skill.mining,
.alchemy => Character.Skill.alchemy,
};
}
};
skill: Skill,
level: u64,
quantity: u64,
items: Items,
pub fn parse(store: *Store, obj: json.ObjectMap) !Craft {
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Craft {
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;
@ -27,7 +62,7 @@ pub fn parse(store: *Store, obj: json.ObjectMap) !Craft {
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return Craft{
.skill = SkillUtils.fromString(skill) orelse return error.InvalidSkill,
.skill = Skill.fromString(skill) orelse return error.InvalidSkill,
.level = @intCast(level),
.quantity = @intCast(quantity),
.items = try Items.parse(store, items)

View File

@ -1,16 +1,16 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const DropRate = @This();
item_id: Store.CodeId,
item: Store.Id,
rate: u64,
min_quantity: u64,
max_quantity: u64,
pub fn parse(store: *Store, obj: json.ObjectMap) !DropRate {
pub fn parse(store: *Store, obj: std.json.ObjectMap) !DropRate {
const rate = try json_utils.getIntegerRequired(obj, "rate");
if (rate < 1) {
return error.InvalidRate;
@ -26,33 +26,27 @@ pub fn parse(store: *Store, obj: json.ObjectMap) !DropRate {
return error.InvalidMinQuantity;
}
const code_str = try json_utils.getStringRequired(obj, "code");
const item_id = try store.getCodeId(code_str);
const code = try json_utils.getStringRequired(obj, "code");
const item_id = try store.items.getOrReserveId(code);
return DropRate{
.item_id = item_id,
.item = item_id,
.rate = @intCast(rate),
.min_quantity = @intCast(min_quantity),
.max_quantity = @intCast(max_quantity)
};
}
pub const DropRates = std.BoundedArray(DropRate, 8); // TODO: Maybe rename to "List"?
pub fn parseList(store: *Store, array: json.Array) !DropRates {
var drops = DropRates.init(0) catch unreachable;
for (array.items) |drop_value| {
pub fn parseDrops(store: *Store, array: std.json.Array, drops: []DropRate) !usize {
for (0.., array.items) |i, drop_value| {
const drop_obj = json_utils.asObject(drop_value) orelse return error.InvalidObject;
try drops.append(try DropRate.parse(store, drop_obj));
}
return drops;
}
pub fn doesListContain(drops: *DropRates, item_id: Store.CodeId) bool {
for (drops.constSlice()) |drop| {
if (drop.item_id == item_id) {
return true;
if (i >= drops.len) {
return error.Overflow;
}
drops[i] = try DropRate.parse(store, drop_obj);
}
return false;
return array.items.len;
}

View File

@ -2,14 +2,23 @@ const std = @import("std");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const CombatStats = @This();
const ElementalStat = @This();
pub const Element = enum {
water,
fire,
earth,
air,
};
pub const Array = std.EnumArray(ElementalStat.Element, ElementalStat);
attack: i64,
damage: i64,
resistance: i64,
pub fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
return CombatStats{
pub fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !ElementalStat {
return ElementalStat{
.attack = try json_utils.getIntegerRequired(object, attack),
.damage = try json_utils.getIntegerRequired(object, damage),
.resistance = try json_utils.getIntegerRequired(object, resistance),

View File

@ -1,28 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const ItemId = Store.ItemId;
const Cooldown = @import("./cooldown.zig");
const EquipRequest = @This();
cooldown: Cooldown,
item: ItemId,
pub fn parse(store: *Store, obj: json.ObjectMap) !EquipRequest {
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 store.getItemId(item_code);
// TODO: Might as well save information about time, because full details about it are given
return EquipRequest{
.cooldown = try Cooldown.parse(cooldown),
.item = item_id
};
}

View File

@ -0,0 +1,37 @@
// zig fmt: off
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Store = @import("../store.zig");
const Character = @import("./character.zig");
const Cooldown = @import("./cooldown.zig");
const Item = @import("./item.zig");
const EquipResult = @This();
pub const Slot = Character.Equipment.SlotId;
cooldown: Cooldown,
slot: Slot,
item: Item,
character: Character,
fn parse(store: *Store, obj: std.json.ObjectMap) !EquipResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const item = json_utils.getObject(obj, "item") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const slot = try json_utils.getStringRequired(obj, "slot");
return EquipResult{
.character = try Character.parse(store, character),
.cooldown = try Cooldown.parse(store, cooldown),
.item = try Item.parse(store, item),
.slot = Slot.fromString(slot) orelse return error.InvalidSlot
};
}
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !EquipResult {
const result = try parse(store, obj);
_ = try store.characters.appendOrUpdate(result.character);
_ = try store.items.appendOrUpdate(result.item);
return result;
}

View File

@ -1,25 +1,39 @@
// zig fmt: off
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Store = @import("../store.zig");
const json = std.json;
const Item = @import("./item.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Equipment = @This();
const CodeId = Store.CodeId;
pub const Slot = struct {
item: ?Store.Id = null,
quantity: u64 = 0,
pub const Consumable = struct {
code_id: ?CodeId,
quantity: i64,
fn parse(store: *Store, obj: std.json.ObjectMap, name: []const u8) !Slot {
const item_code = try json_utils.getStringRequired(obj, name);
if (item_code.len == 0) {
return Slot{};
}
fn parse(store: *Store, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
return Consumable{
.code_id = try store.getCodeIdJson(obj, name),
.quantity = try json_utils.getIntegerRequired(obj, quantity),
return Slot{
.item = try store.items.getOrReserveId(item_code),
.quantity = 1
};
}
fn parseWithQuantity(store: *Store, obj: std.json.ObjectMap, name: []const u8, quantity: []const u8) !Slot {
var slot = try Slot.parse(store, obj, name);
if (slot.item != null) {
slot.quantity = try json_utils.getPositiveIntegerRequired(obj, quantity);
}
return slot;
}
};
pub const Slot = enum {
pub const SlotId = enum {
weapon,
shield,
helmet,
@ -31,61 +45,56 @@ pub const Slot = enum {
amulet,
artifact1,
artifact2,
consumable1,
consumable2,
artifact3,
utility1,
utility2,
fn name(self: Slot) []const u8 {
return switch (self) {
.weapon => "weapon",
.shield => "shield",
.helmet => "helmet",
.body_armor => "body_armor",
.leg_armor => "leg_armor",
.boots => "boots",
.ring1 => "ring1",
.ring2 => "ring2",
.amulet => "amulet",
.artifact1 => "artifact1",
.artifact2 => "artifact2",
.consumable1 => "consumable1",
.consumable2 => "consumable2",
};
const Utils = EnumStringUtils(SlotId, .{
.{ "weapon" , SlotId.weapon },
.{ "shield" , SlotId.shield },
.{ "helmet" , SlotId.helmet },
.{ "body_armor", SlotId.body_armor },
.{ "leg_armor" , SlotId.leg_armor },
.{ "boots" , SlotId.boots },
.{ "ring1" , SlotId.ring1 },
.{ "ring2" , SlotId.ring2 },
.{ "amulet" , SlotId.amulet },
.{ "artifact1" , SlotId.artifact1 },
.{ "artifact2" , SlotId.artifact2 },
.{ "artifact3" , SlotId.artifact3 },
.{ "utility1" , SlotId.utility1 },
.{ "utility2" , SlotId.utility2 },
});
pub const toString = Utils.toString;
pub const fromString = Utils.fromString;
pub fn canHoldManyItems(self: SlotId) bool {
return self == .utility1 or self == .utility2;
}
};
weapon: ?CodeId,
shield: ?CodeId,
helmet: ?CodeId,
body_armor: ?CodeId,
leg_armor: ?CodeId,
boots: ?CodeId,
pub const Slots = std.EnumArray(SlotId, Slot);
ring1: ?CodeId,
ring2: ?CodeId,
amulet: ?CodeId,
slots: Slots,
artifact1: ?CodeId,
artifact2: ?CodeId,
artifact3: ?CodeId,
consumable1: Consumable,
consumable2: Consumable,
pub fn parse(store: *Store, obj: json.ObjectMap) !Equipment {
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Equipment {
return Equipment{
.weapon = try store.getCodeIdJson(obj, "weapon_slot"),
.shield = try store.getCodeIdJson(obj, "shield_slot"),
.helmet = try store.getCodeIdJson(obj, "helmet_slot"),
.body_armor = try store.getCodeIdJson(obj, "body_armor_slot"),
.leg_armor = try store.getCodeIdJson(obj, "leg_armor_slot"),
.boots = try store.getCodeIdJson(obj, "boots_slot"),
.ring1 = try store.getCodeIdJson(obj, "ring1_slot"),
.ring2 = try store.getCodeIdJson(obj, "ring2_slot"),
.amulet = try store.getCodeIdJson(obj, "amulet_slot"),
.artifact1 = try store.getCodeIdJson(obj, "artifact1_slot"),
.artifact2 = try store.getCodeIdJson(obj, "artifact2_slot"),
.artifact3 = try store.getCodeIdJson(obj, "artifact3_slot"),
.consumable1 = try Consumable.parse(store, obj, "consumable1_slot", "consumable1_slot_quantity"),
.consumable2 = try Consumable.parse(store, obj, "consumable2_slot", "consumable2_slot_quantity"),
.slots = Slots.init(.{
.weapon = try Slot.parse(store, obj, "weapon_slot"),
.shield = try Slot.parse(store, obj, "shield_slot"),
.helmet = try Slot.parse(store, obj, "helmet_slot"),
.body_armor = try Slot.parse(store, obj, "body_armor_slot"),
.leg_armor = try Slot.parse(store, obj, "leg_armor_slot"),
.boots = try Slot.parse(store, obj, "boots_slot"),
.ring1 = try Slot.parse(store, obj, "ring1_slot"),
.ring2 = try Slot.parse(store, obj, "ring2_slot"),
.amulet = try Slot.parse(store, obj, "amulet_slot"),
.artifact1 = try Slot.parse(store, obj, "artifact1_slot"),
.artifact2 = try Slot.parse(store, obj, "artifact2_slot"),
.artifact3 = try Slot.parse(store, obj, "artifact3_slot"),
.utility1 = try Slot.parseWithQuantity(store, obj, "utility1_slot", "utility1_slot_quantity"),
.utility2 = try Slot.parseWithQuantity(store, obj, "utility2_slot", "utility2_slot_quantity"),
})
};
}

View File

@ -1,46 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Fight = @This();
pub const Drops = BoundedSlotsArray(8);
xp: u64,
gold: u64,
drops: Drops,
won: bool,
pub fn parse(store: *Store, obj: json.ObjectMap) !Fight {
const result = try json_utils.getStringRequired(obj, "result");
var won = false;
if (std.mem.eql(u8, result, "win")) {
won = true;
} else if (std.mem.eql(u8, result, "lose")) {
won = false;
} else {
return error.InvalidProperty;
}
const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
const xp = try json_utils.getIntegerRequired(obj, "xp");
if (xp < 0) {
return error.InvalidXp;
}
const gold = try json_utils.getIntegerRequired(obj, "gold");
if (gold < 0) {
return error.InvalidGold;
}
return Fight{
.xp = @intCast(xp),
.gold = @intCast(gold),
.drops = try Drops.parse(store, drops_obj),
.won = won,
};
}

43
api/schemas/ge_order.zig Normal file
View File

@ -0,0 +1,43 @@
// zig fmt: off
const std = @import("std");
const Character = @import("./character.zig");
const json_utils = @import("../json_utils.zig");
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const Store = @import("../store.zig");
const Item = @import("./item.zig");
const GEOrder = @This();
pub const max_id_size = 32;
pub const Id = std.BoundedArray(u8, max_id_size);
pub const Account = Character.Account;
pub const Code = Item.Code;
id: Id,
seller: Account,
item_id: Store.Id,
quantity: u64,
price: u64,
created_at: f64,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !GEOrder {
const id = try json_utils.getStringRequired(obj, "id");
const seller = try json_utils.getStringRequired(obj, "seller");
const code = try json_utils.getStringRequired(obj, "code");
const quantity = try json_utils.getPositiveIntegerRequired(obj, "quantity");
const price = try json_utils.getPositiveIntegerRequired(obj, "price");
const created_at = try json_utils.getStringRequired(obj, "created_at");
return GEOrder{
.id = try Id.fromSlice(id),
.seller = try Account.fromSlice(seller),
.item_id = try store.items.getOrReserveId(code),
.quantity = quantity,
.price = price,
.created_at = parseDateTime(created_at) orelse return error.InvalidDataTime,
};
}
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return try store.ge_orders.appendOrUpdate(try parse(store, obj));
}

View File

@ -1,15 +1,14 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const json = std.json;
const Allocator = std.mem.Allocator;
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const Craft = @import("./craft.zig");
const Item = @This();
pub const Type = enum {
utility,
consumable,
body_armor,
weapon,
@ -22,51 +21,87 @@ pub const Type = enum {
ring,
artifact,
currency,
};
pub const TypeUtils = EnumStringUtils(Type, .{
.{ "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 },
});
name: []u8,
code_id: Store.CodeId,
const Utils = EnumStringUtils(Type, .{
.{ "utility" , .consumable },
.{ "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 toString = Utils.toString;
pub const fromString = Utils.fromString;
};
pub const max_code_size = 32;
pub const Name = std.BoundedArray(u8, 32);
pub const Code = std.BoundedArray(u8, max_code_size);
pub const Subtype = std.BoundedArray(u8, 32);
pub const Description = std.BoundedArray(u8, 128);
name: Name,
code: Code,
level: u64,
type: Type,
subtype: []u8,
description: []u8,
subtype: Subtype,
description: Description,
craft: ?Craft,
// TODO: effects
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Item {
pub fn parse(store: *Store, obj: std.json.ObjectMap) !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");
const code = try json_utils.getStringRequired(obj, "code");
const name = try json_utils.getStringRequired(obj, "name");
const subtype = try json_utils.getStringRequired(obj, "subtype");
const description = try json_utils.getStringRequired(obj, "description");
const item_type = try json_utils.getStringRequired(obj, "type");
const craft = json_utils.getObject(obj, "craft");
return Item{
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
.name = try Name.fromSlice(name),
.code = try Code.fromSlice(code),
.level = @intCast(level),
.type = TypeUtils.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,
.type = Type.fromString(item_type) orelse return error.InvalidType,
.subtype = try Subtype.fromSlice(subtype),
.description = try Description.fromSlice(description),
.craft = if (craft != null) try Craft.parse(store, craft.?) else null
};
}
pub fn deinit(self: Item, allocator: Allocator) void {
allocator.free(self.name);
allocator.free(self.subtype);
allocator.free(self.description);
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return try store.items.appendOrUpdate(try Item.parse(store, obj));
}
pub fn format(
self: Item,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{s}{{ ", .{ @typeName(Item) });
try writer.print(".name = \"{s}\", ", .{ self.name.slice() });
try writer.print(".code = \"{s}\", ", .{ self.code.slice() });
try writer.print(".level = {}, ", .{ self.level });
try writer.print(".type = {}, ", .{ self.type });
try writer.print(".subtype = \"{s}\", ", .{ self.subtype.slice() });
try writer.print(".description = \"{s}\", ", .{ self.description.slice() });
if (self.craft) |craft| {
try writer.print(".craft = {}, ", .{ craft });
}
try writer.writeAll("}");
}

View File

@ -1,29 +0,0 @@
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Store = @import("../store.zig");
const json = std.json;
const ItemQuantity = @This();
id: Store.CodeId,
quantity: u64,
pub fn init(id: Store.CodeId, quantity: u64) ItemQuantity {
return ItemQuantity{
.id = id,
.quantity = quantity
};
}
pub fn parse(store: *Store, slot_obj: json.ObjectMap) !?ItemQuantity {
const code = try json_utils.getStringRequired(slot_obj, "code");
if (code.len == 0) return null;
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
if (quantity < 0) return error.InvalidQuantity;
return ItemQuantity{
.id = try store.getCodeId(code),
.quantity = @intCast(quantity),
};
}

View File

@ -1,34 +1,98 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const Position = @import("../position.zig");
const Position = @import("./position.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Monster = @import("./monster.zig");
const Resource = @import("./resource.zig");
const Craft = @import("./craft.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Map = @This();
const MapContent = @import("./map_content.zig");
pub const Name = std.BoundedArray(u8, 16);
name: []u8,
skin: []u8,
pub const max_skin_size = 32;
pub const Skin = std.BoundedArray(u8, max_skin_size);
pub const Content = struct {
pub const Type = enum {
monster,
resource,
workshop,
bank,
grand_exchange,
tasks_master,
santa_claus,
const Utils = EnumStringUtils(Type, .{
.{ "monster" , Type.monster },
.{ "resource" , Type.resource },
.{ "workshop" , Type.workshop },
.{ "bank" , Type.bank },
.{ "grand_exchange", Type.grand_exchange },
.{ "tasks_master" , Type.tasks_master },
.{ "santa_claus" , Type.santa_claus },
});
pub const fromString = Utils.fromString;
pub const toString = Utils.toString;
};
pub const max_code_size = size: {
var max: usize = 0;
max = @max(max, Monster.max_code_size);
max = @max(max, Resource.max_code_size);
for (std.meta.fields(Craft.Skill)) |field| {
max = @max(max, Craft.Skill.toString(@enumFromInt(field.value)).len);
}
max = @max(max, "bank".len);
max = @max(max, "grand_exchange".len);
// TODO: max type 'tasks_master'
break :size max;
};
pub const Code = std.BoundedArray(u8, max_code_size);
type: Type,
code: Code,
};
name: Name,
skin: Skin,
position: Position,
content: ?MapContent,
content: ?Content,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Map {
const content = json_utils.getObject(obj, "content");
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Map {
_ = store;
const x = json_utils.getInteger(obj, "x") orelse return error.MissingProperty;
const y = json_utils.getInteger(obj, "y") orelse return error.MissingProperty;
const name = try json_utils.getStringRequired(obj, "name");
const skin = try json_utils.getStringRequired(obj, "skin");
const x = try json_utils.getIntegerRequired(obj, "x");
const y = try json_utils.getIntegerRequired(obj, "y");
var content: ?Content = null;
if (json_utils.getObject(obj, "content")) |content_obj| {
const content_code = try json_utils.getStringRequired(content_obj, "code");
const content_type = try json_utils.getStringRequired(content_obj, "type");
content = Content{
.code = try Content.Code.fromSlice(content_code),
.type = Content.Type.fromString(content_type) orelse return error.InvalidContentType
};
}
return Map{
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
.name = try Name.fromSlice(name),
.skin = try Skin.fromSlice(skin),
.position = Position.init(x, y),
.content = if (content) |c| try MapContent.parse(store, c) else null
.content = content
};
}
pub fn deinit(self: Map, allocator: Allocator) void {
allocator.free(self.name);
allocator.free(self.skin);
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Position {
return store.appendOrUpdateMap(try parse(store, obj));
}
pub fn parseAndAppendObject(store: *Store, obj: std.json.ObjectMap) !Map {
const position = try parseAndAppend(store, obj);
return store.getMap(position).?.*;
}

View File

@ -1,37 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const MapContent = @This();
pub const Type = enum {
monster,
resource,
workshop,
bank,
grand_exchange,
tasks_master,
};
pub const TypeUtils = EnumStringUtils(Type, .{
.{ "monster" , Type.monster },
.{ "resource" , Type.resource },
.{ "workshop" , Type.workshop },
.{ "bank" , Type.bank },
.{ "grand_exchange", Type.grand_exchange },
.{ "tasks_master" , Type.tasks_master },
});
type: Type,
code_id: Store.CodeId,
pub fn parse(store: *Store, obj: json.ObjectMap) !MapContent {
const content_type = json_utils.getString(obj, "type") orelse return error.MissingProperty;
return MapContent{
.type = TypeUtils.fromString(content_type) orelse return error.InvalidContentType,
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty
};
}

View File

@ -1,19 +1,24 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Character = @import("./character.zig");
const DropRate = @import("./drop_rate.zig");
const DropRates = DropRate.DropRates;
const Element = Character.Element;
const Monster = @This();
pub const Name = std.BoundedArray(u8, 16);
pub const max_code_size = 16;
pub const Code = std.BoundedArray(u8, max_code_size);
pub const ElementalStats = struct {
attack: i64,
resistance: i64,
pub fn parse(object: json.ObjectMap, attack: []const u8, resistance: []const u8) !ElementalStats {
pub fn parse(object: std.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),
@ -21,60 +26,50 @@ pub const ElementalStats = struct {
}
};
name: []u8,
code_id: Store.CodeId,
pub const ElementalStatsArray = std.EnumArray(Element, ElementalStats);
pub const Drops = std.BoundedArray(DropRate, 16);
name: Name,
code: Code,
level: u64,
hp: u64,
elemental_stats: ElementalStatsArray,
min_gold: u64,
max_gold: u64,
drops: Drops,
fire: ElementalStats,
earth: ElementalStats,
water: ElementalStats,
air: ElementalStats,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Monster {
const name = try json_utils.getStringRequired(obj, "name");
const code = try json_utils.getStringRequired(obj, "code");
const level = try json_utils.getPositiveIntegerRequired(obj, "level");
const hp = try json_utils.getPositiveIntegerRequired(obj, "hp");
const min_gold = try json_utils.getPositiveIntegerRequired(obj, "min_gold");
const max_gold = try json_utils.getPositiveIntegerRequired(obj, "max_gold");
drops: DropRates,
const elemental_stats = ElementalStatsArray.init(.{
.water = try ElementalStats.parse(obj, "attack_water", "res_water"),
.fire = try ElementalStats.parse(obj, "attack_fire", "res_fire"),
.earth = try ElementalStats.parse(obj, "attack_earth", "res_earth"),
.air = try ElementalStats.parse(obj, "attack_air", "res_air"),
});
pub fn parse(store: *Store, 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;
}
const drops_array = try json_utils.getArrayRequired(obj, "drops");
var drops = Drops.init(0) catch unreachable;
drops.len = @intCast(try DropRate.parseDrops(store, drops_array, &drops.buffer));
return Monster{
.name = try json_utils.dupeStringRequired(allocator, obj, "name"),
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
.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(store, drops_array)
.name = try Name.fromSlice(name),
.code = try Code.fromSlice(code),
.level = level,
.hp = hp,
.elemental_stats = elemental_stats,
.min_gold = min_gold,
.max_gold = max_gold,
.drops = drops
};
}
pub fn deinit(self: Monster, allocator: Allocator) void {
allocator.free(self.name);
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return store.monsters.appendOrUpdate(try parse(store, obj));
}

View File

@ -0,0 +1,31 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const Character = @import("./character.zig");
const Cooldown = @import("./cooldown.zig");
const Map = @import("./map.zig");
const MoveResult = @This();
cooldown: Cooldown,
destination: Map,
character: Character,
fn parse(store: *Store, obj: std.json.ObjectMap) !MoveResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const destination = json_utils.getObject(obj, "destination") orelse return error.MissingProperty;
return MoveResult{
.character = try Character.parse(store, character),
.destination = try Map.parse(store, destination),
.cooldown = try Cooldown.parse(store, cooldown)
};
}
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !MoveResult {
const result = try parse(store, obj);
_ = try store.characters.appendOrUpdate(result.character);
return result;
}

39
api/schemas/position.zig Normal file
View File

@ -0,0 +1,39 @@
const std = @import("std");
const Position = @This();
x: i64,
y: i64,
pub fn init(x: i64, y: i64) Position {
return Position{ .x = x, .y = y };
}
pub fn zero() Position {
return init(0, 0);
}
pub fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
}
pub fn subtract(self: Position, other: Position) Position {
return init(self.x - other.x, self.y - other.y);
}
pub fn distance(self: Position, other: Position) f32 {
const dx: f32 = @floatFromInt(self.x - other.x);
const dy: f32 = @floatFromInt(self.y - other.y);
return @sqrt(dx * dx + dy * dy);
}
pub fn format(
self: Position,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("Position{{ {}, {} }}", .{ self.x, self.y });
}

View File

@ -1,12 +1,10 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Character = @import("./character.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const DropRate = @import("./drop_rate.zig");
const DropRates = DropRate.DropRates;
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Resource = @This();
@ -14,38 +12,62 @@ pub const Skill = enum {
mining,
woodcutting,
fishing,
};
pub const SkillUtils = EnumStringUtils(Skill, .{
.{ "mining" , .mining },
.{ "woodcutting", .woodcutting },
.{ "fishing" , .fishing },
});
alchemy,
name: []u8,
code_id: Store.CodeId,
const Utils = EnumStringUtils(Skill, .{
.{ "mining" , Skill.mining },
.{ "woodcutting", Skill.woodcutting },
.{ "fishing" , Skill.fishing },
.{ "alchemy" , Skill.alchemy },
});
pub const toString = Utils.toString;
pub const fromString = Utils.fromString;
pub fn toCharacterSkill(self: Skill) Character.Skill {
return switch (self) {
.mining => Character.Skill.mining,
.woodcutting => Character.Skill.woodcutting,
.fishing => Character.Skill.fishing,
.alchemy => Character.Skill.alchemy,
};
}
};
pub const Name = std.BoundedArray(u8, 32);
pub const max_code_size = 32;
pub const Code = std.BoundedArray(u8, max_code_size);
pub const max_drops = 16;
pub const Drop = DropRate;
pub const Drops = std.BoundedArray(DropRate, max_drops);
name: Name,
code: Code,
skill: Skill,
level: u64,
drops: DropRates,
drops: Drops,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Resource {
const drops_array = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Resource {
const name = try json_utils.getStringRequired(obj, "name");
const code = try json_utils.getStringRequired(obj, "code");
const level = try json_utils.getPositiveIntegerRequired(obj, "level");
const skill = try json_utils.getStringRequired(obj, "skill");
const level = try json_utils.getIntegerRequired(obj, "level");
if (level < 0) {
return error.InvalidLevel;
}
const skill_str = try json_utils.getStringRequired(obj, "skill");
const drops_array = try json_utils.getArrayRequired(obj, "drops");
var drops = Drops.init(0) catch unreachable;
drops.len = @intCast(try DropRate.parseDrops(store, drops_array, &drops.buffer));
return Resource{
.name = try json_utils.dupeStringRequired(allocator, obj, "name"),
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
.level = @intCast(level),
.skill = SkillUtils.fromString(skill_str) orelse return error.InvalidSkill,
.drops = try DropRate.parseList(store, drops_array)
.name = try Name.fromSlice(name),
.code = try Code.fromSlice(code),
.level = level,
.skill = Skill.fromString(skill) orelse return error.InvalidSkill,
.drops = drops
};
}
pub fn deinit(self: Resource, allocator: Allocator) void {
allocator.free(self.name);
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return store.resources.appendOrUpdate(try parse(store, obj));
}

152
api/schemas/simple_item.zig Normal file
View File

@ -0,0 +1,152 @@
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Store = @import("../store.zig");
const assert = std.debug.assert;
const SimpleItem = @This();
id: Store.Id,
quantity: u64,
pub fn init(id: Store.Id, quantity: u64) SimpleItem {
return SimpleItem{ .id = id, .quantity = quantity };
}
pub fn parse(store: *Store, slot_obj: std.json.ObjectMap) !?SimpleItem {
const code = try json_utils.getStringRequired(slot_obj, "code");
if (code.len == 0) return null;
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
if (quantity < 0) return error.InvalidQuantity;
return SimpleItem{
.id = try store.items.getOrReserveId(code),
.quantity = @intCast(quantity),
};
}
pub fn BoundedArray(comptime slot_count: u32) type {
const Items = std.BoundedArray(SimpleItem, slot_count);
return struct {
items: Items = .{ .len = 0 },
pub fn init() @This() {
return @This(){};
}
pub fn parse(store: *Store, slots_array: std.json.Array) !@This() {
var slots = Items.init(0) catch unreachable;
for (slots_array.items) |slot_value| {
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
if (try SimpleItem.parse(store, slot_obj)) |slot| {
try slots.append(slot);
}
}
return @This(){ .items = slots };
}
fn findSlotIndex(self: *const @This(), id: Store.Id) ?usize {
for (0.., self.items.slice()) |i, *slot| {
if (slot.id == id) {
return i;
}
}
return null;
}
fn findSlot(self: *@This(), id: Store.Id) ?*SimpleItem {
if (self.findSlotIndex(id)) |index| {
return &self.items.buffer[index];
}
return null;
}
pub fn remove(self: *@This(), id: Store.Id, quantity: u64) void {
const slot_index = self.findSlotIndex(id) orelse unreachable;
const slot = self.items.get(slot_index);
assert(slot.quantity >= quantity);
slot.quantity -= quantity;
if (slot.quantity == 0) {
self.items.swapRemove(slot_index);
}
}
pub fn add(self: *@This(), id: Store.Id, quantity: u64) !void {
if (quantity == 0) return;
if (self.findSlot(id)) |slot| {
slot.quantity += quantity;
} else {
try self.items.append(SimpleItem.init(id, quantity));
}
}
pub fn addAssumeCapacity(self: *@This(), id: Store.Id, quantity: u64) void {
if (quantity == 0) return;
if (self.findSlot(id)) |slot| {
slot.quantity += quantity;
} else {
self.items.appendAssumeCapacity(SimpleItem.init(id, quantity));
}
}
pub fn addSlice(self: *@This(), items: []const SimpleItem) !void {
for (items) |item| {
try self.add(item.id, item.quantity);
}
}
pub fn removeSlice(self: *@This(), items: []const SimpleItem) void {
for (items) |item| {
self.remove(item.id, item.quantity);
}
}
pub fn getQuantity(self: *const @This(), id: Store.Id) u64 {
if (self.findSlotIndex(id)) |index| {
return self.items.get(index).quantity;
}
return 0;
}
pub fn totalQuantity(self: *const @This()) u64 {
var count: u64 = 0;
for (self.items.constSlice()) |slot| {
count += slot.quantity;
}
return count;
}
pub fn slice(self: @This()) []const SimpleItem {
return self.items.slice();
}
pub fn format(
self: @This(),
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{s}{{ ", .{@typeName(@This())});
for (self.items.slice()) |item| {
try writer.print("{}, ", .{item});
}
try writer.writeAll("}");
}
};
}

View File

@ -1,22 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Item = @import("./item.zig");
const SingleItem = @This();
item: Item,
// TODO: Grand exchange
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !SingleItem {
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 SingleItem{
.item = try Item.parse(store, item_obj, allocator),
};
}

View File

@ -1,18 +0,0 @@
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
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 },
});

View File

@ -1,27 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const SkillInfo = @import("./skill_info.zig");
const SkillData = @This();
cooldown: Cooldown,
details: SkillInfo,
character: Character,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !SkillData {
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 SkillData{
.cooldown = try Cooldown.parse(cooldown),
.details = try SkillInfo.parse(store, details),
.character = try Character.parse(store, character, allocator)
};
}

View File

@ -1,25 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Items = BoundedSlotsArray(8);
const SkillInfo = @This();
xp: u64,
items: Items,
pub fn parse(store: *Store, obj: json.ObjectMap) !SkillInfo {
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
const xp = try json_utils.getIntegerRequired(obj, "xp");
if (xp < 0) {
return error.InvalidXp;
}
return SkillInfo{
.xp = @intCast(xp),
.items = try Items.parse(store, items),
};
}

View File

@ -0,0 +1,23 @@
// zig fmt: off
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Resource = @import("./resource.zig");
const Store = @import("../store.zig");
const SimpleItem = @import("./simple_item.zig");
const SkillInfoDetails = @This();
pub const Items = SimpleItem.BoundedArray(Resource.max_drops);
xp: u64,
items: Items,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !SkillInfoDetails {
const xp = try json_utils.getPositiveIntegerRequired(obj, "xp");
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return SkillInfoDetails{
.xp = xp,
.items = try Items.parse(store, items)
};
}

View File

@ -1,17 +0,0 @@
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const SkillStats = @This();
level: i64,
xp: i64,
max_xp: i64,
pub fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
return SkillStats{
.level = try json_utils.getIntegerRequired(object, level),
.xp = try json_utils.getIntegerRequired(object, xp),
.max_xp = try json_utils.getIntegerRequired(object, max_xp),
};
}

View File

@ -0,0 +1,33 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const Character = @import("./character.zig");
const Cooldown = @import("./cooldown.zig");
const SkillInfoDetails = @import("./skill_info_details.zig");
const SkillUsageResult = @This();
pub const Details = SkillInfoDetails;
cooldown: Cooldown,
details: Details,
character: Character,
fn parse(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
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 SkillUsageResult{
.character = try Character.parse(store, character),
.cooldown = try Cooldown.parse(store, cooldown),
.details = try SkillInfoDetails.parse(store, details)
};
}
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
const result = try parse(store, obj);
_ = try store.characters.appendOrUpdate(result.character);
return result;
}

View File

@ -1,108 +0,0 @@
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Store = @import("../store.zig");
const assert = std.debug.assert;
const json = std.json;
const ItemQuantity = @import("./item_quantity.zig");
pub fn BoundedSlotsArray(comptime slot_count: u32) type {
const Slots = std.BoundedArray(ItemQuantity, slot_count);
const CodeId = Store.CodeId;
return struct {
slots: Slots,
pub fn init() @This() {
return @This(){
.slots = Slots.init(0) catch unreachable
};
}
pub fn parse(api: *Store, slots_array: json.Array) !@This() {
var slots = Slots.init(0) catch unreachable;
for (slots_array.items) |slot_value| {
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
if (try ItemQuantity.parse(api, slot_obj)) |slot| {
try slots.append(slot);
}
}
return @This(){ .slots = slots };
}
fn findSlotIndex(self: *const @This(), id: CodeId) ?usize {
for (0.., self.slots.slice()) |i, *slot| {
if (slot.id == id) {
return i;
}
}
return null;
}
fn findSlot(self: *@This(), id: CodeId) ?*ItemQuantity {
if (self.findSlotIndex(id)) |index| {
return &self.slots.buffer[index];
}
return null;
}
pub fn remove(self: *@This(), id: CodeId, quantity: u64) void {
const slot_index = self.findSlotIndex(id) orelse unreachable;
const slot = self.slots.get(slot_index);
assert(slot.quantity >= quantity);
slot.quantity -= quantity;
if (slot.quantity == 0) {
self.slots.swapRemove(slot_index);
}
}
pub fn add(self: *@This(), id: CodeId, quantity: u64) !void {
if (quantity == 0) return;
if (self.findSlot(id)) |slot| {
slot.quantity += quantity;
} else {
try self.slots.append(ItemQuantity.init(id, quantity));
}
}
pub fn addSlice(self: *@This(), items: []const ItemQuantity) void {
for (items) |item| {
self.add(item.id, item.quantity);
}
}
pub fn removeSlice(self: *@This(), items: []const ItemQuantity) void {
for (items) |item| {
self.remove(item.id, item.quantity);
}
}
pub fn getQuantity(self: *const @This(), id: CodeId) u64 {
if (self.findSlotIndex(id)) |index| {
return self.slots.get(index).quantity;
}
return 0;
}
pub fn totalQuantity(self: *const @This()) u64 {
var count: u64 = 0;
for (self.slots.constSlice()) |slot| {
count += slot.quantity;
}
return count;
}
pub fn slice(self: *@This()) []ItemQuantity {
return self.slots.slice();
}
};
}

View File

@ -1,32 +1,102 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const json = std.json;
const Allocator = std.mem.Allocator;
const ServerStatus = @This();
allocator: Allocator,
status: []const u8,
version: []const u8,
characters_online: u64,
pub const Status = std.BoundedArray(u8, 32);
pub const Version = std.BoundedArray(u8, 16);
pub const Date = std.BoundedArray(u8, 10);
pub const Announcement = struct {
pub const Message = std.BoundedArray(u8, 64);
pub fn parse(store: *Store, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
message: Message,
created_at: f64
};
pub const Announcements = std.BoundedArray(Announcement, 16);
status: Status,
version: Version,
characters_online: u64,
max_level: u64,
server_time: f64,
last_wipe: Date,
next_wipe: Date,
announcements: Announcements,
fn parseAnnouncements(array: json.Array) !Announcements {
var announcements = Announcements.init(0) catch unreachable;
for (array.items) |item| {
const obj = json_utils.asObject(item) orelse return error.InvalidAnnouncement;
const message = try json_utils.getStringRequired(obj, "message");
const created_at = try json_utils.getStringRequired(obj, "created_at");
try announcements.append(Announcement{
.message = try Announcement.Message.fromSlice(message),
.created_at = parseDateTime(created_at) orelse return error.InvalidDataTime,
});
}
return announcements;
}
pub fn parse(store: *Store, obj: json.ObjectMap) !ServerStatus {
_ = store;
const characters_online = json_utils.getInteger(object, "characters_online") orelse return error.MissingProperty;
const characters_online = json_utils.getInteger(obj, "characters_online") orelse return error.MissingProperty;
if (characters_online < 0) {
return error.InvalidCharactersOnline;
}
const max_level = try json_utils.getIntegerRequired(obj, "max_level");
if (max_level < 0) {
return error.InvalidMaxLevel;
}
const status = try json_utils.getStringRequired(obj, "status");
const version = try json_utils.getStringRequired(obj, "version");
const last_wipe = try json_utils.getStringRequired(obj, "last_wipe");
const next_wipe = try json_utils.getStringRequired(obj, "next_wipe");
const server_time = try json_utils.getStringRequired(obj, "server_time");
const announcements = json_utils.getArray(obj, "announcements") orelse return error.MissingProperty;
return ServerStatus{
.allocator = allocator,
.characters_online = @intCast(characters_online),
.status = (try json_utils.dupeString(allocator, object, "status")) orelse return error.MissingStatus,
.version = (try json_utils.dupeString(allocator, object, "version")) orelse return error.MissingVersion
.status = try Status.fromSlice(status),
.version = try Version.fromSlice(version),
.next_wipe = try Date.fromSlice(next_wipe),
.last_wipe = try Date.fromSlice(last_wipe),
.server_time = parseDateTime(server_time) orelse return error.InvalidaDateTime,
.announcements = try parseAnnouncements(announcements),
.max_level = @intCast(max_level)
};
}
pub fn deinit(self: ServerStatus) void {
self.allocator.free(self.status);
self.allocator.free(self.version);
pub fn format(
self: ServerStatus,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{s}{{ ", .{ @typeName(ServerStatus) });
try writer.print(".status = \"{s}\", ", .{ self.status.slice() });
try writer.print(".version = \"{s}\", ", .{ self.version.slice() });
try writer.print(".characters_online = {}, ", .{ self.characters_online });
try writer.print(".max_level = {}, ", .{ self.max_level });
try writer.print(".server_time = {}, ", .{ self.server_time });
try writer.print(".last_wipe = \"{s}\", ", .{ self.last_wipe.slice() });
try writer.print(".next_wipe = \"{s}\", ", .{ self.next_wipe.slice() });
try writer.writeAll(".announcements = .{ ... }, ");
try writer.writeAll("}");
}

View File

@ -1,36 +1,45 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const json = std.json;
const Task = @This();
pub const Type = enum {
monsters,
resources,
crafts
};
pub const TypeUtils = EnumStringUtils(Type, .{
.{ "monsters" , Type.monsters },
.{ "resources", Type.resources },
.{ "crafts" , Type.crafts },
});
crafts,
code_id: Store.CodeId,
const Utils = EnumStringUtils(Type, .{
.{ "monsters" , Type.monsters },
.{ "resources", Type.resources },
.{ "crafts" , Type.crafts },
});
pub const fromString = Utils.fromString;
pub const toString = Utils.toString;
};
pub const Code = std.BoundedArray(u8, 32);
code: Code,
type: Type,
total: u64,
pub fn parse(store: *Store, obj: json.ObjectMap) !Task {
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Task {
_ = store;
const code = try json_utils.getStringRequired(obj, "code");
const task_type = try json_utils.getStringRequired(obj, "type");
const total = try json_utils.getIntegerRequired(obj, "total");
if (total < 0) {
return error.InvalidTaskTotal;
}
const total = try json_utils.getPositiveIntegerRequired(obj, "total");
return Task{
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
.type = TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
.total = @intCast(total)
.code = try Code.fromSlice(code),
.type = Type.fromString(task_type) orelse return error.InvalidTaskType,
.total = total
};
}
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return store.tasks.appendOrUpdate(try parse(store, obj));
}

View File

@ -1,27 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const Task = @import("./task.zig");
const TaskData = @This();
cooldown: Cooldown,
character: Character,
task: Task,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !TaskData {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const task = json_utils.getObject(obj, "task") orelse return error.MissingProperty;
return TaskData{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(store, character, allocator),
.task = try Task.parse(store, task)
};
}

View File

@ -1,27 +0,0 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const ItemQuantity = @import("./item_quantity.zig");
const TaskRewardData = @This();
cooldown: Cooldown,
character: Character,
reward: ItemQuantity,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !TaskRewardData {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const task = json_utils.getObject(obj, "task") orelse return error.MissingProperty;
return TaskRewardData{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(store, character, allocator),
.reward = (try ItemQuantity.parse(store, task)) orelse return error.MissinReward
};
}

File diff suppressed because it is too large Load Diff

28
api/stb_image/root.zig Normal file
View File

@ -0,0 +1,28 @@
const std = @import("std");
const assert = std.debug.assert;
extern fn zig_stbi_load_from_memory(buffer: [*]const u8, len: i32, x: ?*i32, y: ?*i32, comp: ?*i32, req_comp: i32) callconv(.C) ?[*]u8;
extern fn zig_stbi_image_free(buffer: ?*anyopaque) callconv(.C) void;
pub const Image = struct {
width: u32,
height: u32,
rgba: []u8,
pub fn deinit(self: Image) void {
zig_stbi_image_free(self.rgba.ptr);
}
};
pub fn load(buffer: []const u8) !Image {
var width: i32 = 0;
var height: i32 = 0;
const image_rgba = zig_stbi_load_from_memory(buffer.ptr, @intCast(buffer.len), &width, &height, null, 4);
if (image_rgba == null) {
return error.PNGDecode;
}
errdefer zig_stbi_image_free(image_rgba);
const byte_count: u32 = @intCast(width * height * 4);
return Image{ .width = @intCast(width), .height = @intCast(height), .rgba = image_rgba.?[0..byte_count] };
}

23
api/stb_image/stb_image.c Normal file
View File

@ -0,0 +1,23 @@
#include <stdint.h>
#include <stdio.h>
extern void *stb_image_zig_malloc(uint32_t amount);
extern void *stb_image_zig_realloc(void *mem, uint32_t amount);
extern void stb_image_zig_free(void *mem);
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_STDIO
#define STBI_ONLY_PNG
#define STB_IMAGE_STATIC
#include "stb_image.h"
void zig_stbi_image_free(void *retval_from_stbi_load)
{
return stbi_image_free(retval_from_stbi_load);
}
stbi_uc *zig_stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp)
{
return stbi_load_from_memory(buffer, len, x, y, comp, req_comp);
}

7988
api/stb_image/stb_image.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,422 +1,642 @@
// zig fmt: off
const std = @import("std");
const json_utils = @import("json_utils.zig");
const Server = @import("./server.zig");
const s2s = @import("s2s");
const json = std.json;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Item = @import("./schemas/item.zig");
const SimpleItem = @import("./schemas/simple_item.zig");
const Character = @import("./schemas/character.zig");
const Task = @import("./schemas/task.zig");
const Monster = @import("./schemas/monster.zig");
const Resource = @import("./schemas/resource.zig");
const Map = @import("./schemas/map.zig");
const GEOrder = @import("./schemas/ge_order.zig");
const Position = @import("./schemas/position.zig");
const Skin = Character.Skin;
pub const Id = usize;
const Store = @This();
const Character = @import("./schemas/character.zig");
const Item = @import("./schemas/item.zig");
const Position = @import("./position.zig");
const Map = @import("./schemas/map.zig");
const Resource = @import("./schemas/resource.zig");
const Monster = @import("./schemas/monster.zig");
const DropRate = @import("./schemas/drop_rate.zig");
pub const CodeId = u16;
const Characters = std.ArrayList(Character);
const ItemsMap = std.StringHashMap(Item);
const MapsMap = std.AutoHashMap(Position, Map);
const ResourcesMap = std.StringHashMap(Resource);
const MonstersMap = std.StringHashMap(Monster);
allocator: Allocator,
codes: std.ArrayList([]u8),
characters: Characters,
items: ItemsMap,
maps: MapsMap,
resources: ResourcesMap,
monsters: MonstersMap,
// TODO: bank
pub fn init(allocator: Allocator) Store {
return Store{
.allocator = allocator,
.codes = std.ArrayList([]u8).init(allocator),
.characters = Characters.init(allocator),
.items = ItemsMap.init(allocator),
.maps = MapsMap.init(allocator),
.resources = ResourcesMap.init(allocator),
.monsters = MonstersMap.init(allocator),
};
}
pub fn deinit(self: *Store) void {
self.clearCodes();
for (self.characters.items) |*char| {
char.deinit();
const max_character_images = std.meta.fields(Skin).len;
const max_character_image_code = size: {
var size = Skin.toString(.men1).len;
for (std.meta.fields(Skin)) |field| {
size = @max(size, Skin.toString(@enumFromInt(field.value)).len);
}
self.characters.deinit();
self.clearItems();
break :size size;
};
self.clearMaps();
pub const Image = struct {
pub const Category = enum {
character,
item,
monster,
map,
resource,
effect
};
self.clearResources();
pub const max_code_size = size: {
var size: usize = 0;
self.clearMonsters();
}
size = @max(size, Item.max_code_size);
size = @max(size, max_character_image_code);
size = @max(size, Monster.max_code_size);
size = @max(size, Resource.max_code_size);
size = @max(size, Map.max_skin_size);
// TODO: effect code size
pub fn getCodeId(self: *Store, code: []const u8) !CodeId {
assert(code.len != 0);
break :size size;
};
pub const Code = std.BoundedArray(u8, max_code_size);
for (0.., self.codes.items) |i, item_code| {
if (std.mem.eql(u8, code, item_code)) {
return @intCast(i);
code: Code,
width: u32,
height: u32,
rgba_offset: u32,
};
const Images = struct {
const ImagesArray = std.ArrayListUnmanaged(Image);
const IdArray = std.ArrayListUnmanaged(Id);
const CategoryMap = std.EnumArray(Image.Category, IdArray);
const Options = struct {
max_rgba_data: u32,
max_items: u32,
max_monsters: u32,
max_maps: u32,
max_resources: u32,
max_effects: u32,
};
rgba_data: []u8,
rgba_fba: std.heap.FixedBufferAllocator,
category_mapping: CategoryMap,
images: ImagesArray,
pub fn initCapacity(allocator: std.mem.Allocator, opts: Options) !Images {
const max_characters = std.meta.fields(Character.Skin).len;
var max_images: u32 = 0;
max_images += @intCast(max_characters);
max_images += opts.max_items;
max_images += opts.max_monsters;
max_images += opts.max_maps;
max_images += opts.max_resources;
max_images += opts.max_effects;
const rgba_data = try allocator.alloc(u8, opts.max_rgba_data);
errdefer allocator.free(rgba_data);
var images = try ImagesArray.initCapacity(allocator, max_images);
errdefer images.deinit(allocator);
var character_images = try IdArray.initCapacity(allocator, max_characters);
errdefer character_images.deinit(allocator);
var item_images = try IdArray.initCapacity(allocator, opts.max_items);
errdefer item_images.deinit(allocator);
var monster_images = try IdArray.initCapacity(allocator, opts.max_monsters);
errdefer monster_images.deinit(allocator);
var map_images = try IdArray.initCapacity(allocator, opts.max_maps);
errdefer map_images.deinit(allocator);
var resource_images = try IdArray.initCapacity(allocator, opts.max_resources);
errdefer resource_images.deinit(allocator);
var effect_images = try IdArray.initCapacity(allocator, opts.max_effects);
errdefer effect_images.deinit(allocator);
const category_mapping = CategoryMap.init(.{
.character = character_images,
.item = item_images,
.monster = monster_images,
.resource = resource_images,
.effect = effect_images,
.map = map_images
});
return Images{
.rgba_data = rgba_data,
.rgba_fba = std.heap.FixedBufferAllocator.init(rgba_data),
.category_mapping = category_mapping,
.images = images
};
}
pub fn deinit(self: *Images, allocator: std.mem.Allocator) void {
allocator.free(self.rgba_data);
self.images.deinit(allocator);
self.rgba_fba.reset();
var iter = self.category_mapping.iterator();
while (iter.next()) |id_array| {
id_array.value.deinit(allocator);
}
}
const code_dupe = try self.allocator.dupe(u8, code);
errdefer self.allocator.free(code_dupe);
try self.codes.append(code_dupe);
pub fn clone(self: *Images, allocator: std.mem.Allocator) !Images {
const rgba_data = try allocator.dupe(self.rgba_data);
errdefer allocator.free(rgba_data);
return @intCast(self.codes.items.len - 1);
}
var images = try self.images.clone(allocator);
errdefer images.deinit(allocator);
var character_images = try self.category_mapping.get(.character).clone(allocator);
errdefer character_images.deinit(allocator);
var item_images = try self.category_mapping.get(.item).clone(allocator);
errdefer item_images.deinit(allocator);
var monster_images = try self.category_mapping.get(.monster).clone(allocator);
errdefer monster_images.deinit(allocator);
var map_images = try self.category_mapping.get(.map).clone(allocator);
errdefer map_images.deinit(allocator);
var resource_images = try self.category_mapping.get(.resource).clone(allocator);
errdefer resource_images.deinit(allocator);
var effect_images = try self.category_mapping.get(.effect).clone(allocator);
errdefer effect_images.deinit(allocator);
const category_mapping = CategoryMap.init(.{
.character = character_images,
.item = item_images,
.monster = monster_images,
.resource = resource_images,
.effect = effect_images,
.map = map_images
});
return Images{
.rgba_data = rgba_data,
.rgba_fba = std.heap.FixedBufferAllocator{
.buffer = rgba_data,
.end_index = self.rgba_fba.end_index
},
.images = images,
.category_mapping = category_mapping
};
}
pub fn get(self: *Images, id: Id) ?*Image {
if (id < self.images.items.len) {
return &self.images.items[id];
}
pub fn getCode(self: *const Store, id: CodeId) ?[]const u8 {
if (id >= self.codes.items.len) {
return null;
}
return self.codes.items[id];
}
pub fn getId(self: *Images, category: Image.Category, code: []const u8) ?Id {
const id_array = self.category_mapping.get(category);
for (id_array.items) |id| {
const image = self.images.items[id];
const image_code = image.code.slice();
if (std.mem.eql(u8, image_code, code)) {
return id;
}
}
pub fn getCodeIdJson(self: *Store, object: json.ObjectMap, name: []const u8) !?CodeId {
const code = try json_utils.getStringRequired(object, name);
if (code.len == 0) {
return null;
}
return try self.getCodeId(code);
}
pub fn getRGBA(self: *Images, id: Id) ?[]u8 {
const image = self.get(id) orelse return null;
const rgba_start = image.rgba_offset;
const rgba_stop = image.rgba_offset + image.width * image.height * 4;
fn clearCodes(self: *Store) void {
for (self.codes.items) |code| {
self.allocator.free(code);
return self.rgba_data[rgba_start..rgba_stop];
}
self.codes.clearAndFree();
pub fn append(self: *Images, category: Image.Category, code: []const u8, width: u32, height: u32) !Id {
if (self.images.unusedCapacitySlice().len == 0) {
return error.OutOfMemory;
}
var image_ids = self.category_mapping.getPtr(category);
if (image_ids.unusedCapacitySlice().len == 0) {
return error.OutOfMemory;
}
const rgba_allocator = self.rgba_fba.allocator();
const rgba = try rgba_allocator.alloc(u8, width * height * 4);
errdefer rgba_allocator.free(rgba);
const image_id = self.images.items.len;
self.images.appendAssumeCapacity(Image{
.rgba_offset = @intCast(@intFromPtr(rgba.ptr) - @intFromPtr(self.rgba_data.ptr)),
.width = width,
.height = height,
.code = try Image.Code.fromSlice(code)
});
errdefer _ = self.images.pop();
image_ids.appendAssumeCapacity(image_id);
return image_id;
}
};
fn Repository(comptime Object: type, comptime name_field: []const u8) type {
const dummy: Object = undefined;
const Name: type = @TypeOf(@field(dummy, name_field));
return struct {
pub const OptionalObject = union(enum) {
reserved: Name,
object: Object
};
const OptionalObjects = std.ArrayListUnmanaged(OptionalObject);
objects: OptionalObjects,
pub fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !@This() {
return @This(){
.objects = try OptionalObjects.initCapacity(allocator, capacity)
};
}
pub fn clone(self: @This(), allocator: std.mem.Allocator) !@This() {
return @This(){
.objects = try self.objects.clone(allocator)
};
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.objects.deinit(allocator);
}
pub fn get(self: *@This(), id: Id) ?*Object {
if (id < self.objects.items.len) {
const item = &self.objects.items[id];
if (item.* == .object) {
return &item.object;
}
}
return null;
}
pub fn remove(self: *@This(), id: Id) bool {
if (id < self.objects.items.len) {
const item = &self.objects.items[id];
if (item.* == .object) {
item.* = .{ .reserved = @field(item.object, name_field) };
return true;
}
}
return false;
}
pub fn getId(self: *@This(), name: []const u8) ?Id {
for (0.., self.objects.items) |id, object| {
const object_name = switch (object) {
.object => |obj| @field(obj, name_field).slice(),
.reserved => |object_name| object_name.slice(),
};
if (std.mem.eql(u8, name, object_name)) {
return id;
}
}
return null;
}
pub fn getName(self: *@This(), id: Id) ?[]const u8 {
if (id < self.objects.items.len) {
return switch (self.objects.items[id]) {
.object => |obj| @field(obj, name_field).slice(),
.reserved => |name| name.slice(),
};
}
return null;
}
pub fn getOrReserveId(self: *@This(), name: []const u8) !Id {
if (self.getId(name)) |id| {
return id;
}
if (self.objects.unusedCapacitySlice().len == 0) {
return error.OutOfMemory;
}
const owned_name = try Name.fromSlice(name);
self.objects.appendAssumeCapacity(.{ .reserved = owned_name });
return self.objects.items.len - 1;
}
pub fn appendOrUpdate(self: *@This(), item: Object) !Id {
if (self.getId(@field(item, name_field).slice())) |id| {
self.objects.items[id] = .{ .object = item };
return id;
} else {
if (self.objects.unusedCapacitySlice().len == 0) {
return error.OutOfMemory;
}
self.objects.appendAssumeCapacity(.{ .object = item });
return self.objects.items.len - 1;
}
}
};
}
// ----------------------- Storing to file ------------------------------
const Items = Repository(Item, "code");
const Characters = Repository(Character, "name");
const Tasks = Repository(Task, "code");
const Monsters = Repository(Monster, "code");
const Resources = Repository(Resource, "code");
const GEOrders = Repository(GEOrder, "id");
const Maps = std.ArrayListUnmanaged(Map);
const Bank = SimpleItem.BoundedArray(64);
items: Items,
characters: Characters,
tasks: Tasks,
monsters: Monsters,
resources: Resources,
images: Images,
maps: Maps,
bank: Bank,
ge_orders: GEOrders,
pub fn init(allocator: std.mem.Allocator) !Store {
const max_items = 512;
const max_characters = 10;
const max_tasks = 32;
const max_monsters = 64;
const max_resources = 32;
const max_maps = 512;
const max_ge_orders = 128;
var items = try Items.initCapacity(allocator, max_items);
errdefer items.deinit(allocator);
var characters = try Characters.initCapacity(allocator, max_characters);
errdefer characters.deinit(allocator);
var tasks = try Tasks.initCapacity(allocator, max_tasks);
errdefer tasks.deinit(allocator);
var monsters = try Monsters.initCapacity(allocator, max_monsters);
errdefer monsters.deinit(allocator);
var resources = try Resources.initCapacity(allocator, max_resources);
errdefer resources.deinit(allocator);
var images = try Images.initCapacity(allocator, .{
.max_rgba_data = 1024 * 1024 * 32,
.max_items = max_items,
.max_effects = 16,
.max_resources = max_resources,
.max_maps = max_maps,
.max_monsters = max_monsters,
});
errdefer images.deinit(allocator);
var maps = try Maps.initCapacity(allocator, max_maps);
errdefer maps.deinit(allocator);
var ge_orders = try GEOrders.initCapacity(allocator, max_ge_orders);
errdefer ge_orders.deinit(allocator);
const bank = Bank.init();
return Store{
.items = items,
.characters = characters,
.tasks = tasks,
.monsters = monsters,
.resources = resources,
.maps = maps,
.images = images,
.bank = bank,
.ge_orders = ge_orders
};
}
pub fn deinit(self: *Store, allocator: std.mem.Allocator) void {
self.items.deinit(allocator);
self.characters.deinit(allocator);
self.tasks.deinit(allocator);
self.monsters.deinit(allocator);
self.resources.deinit(allocator);
self.maps.deinit(allocator);
self.images.deinit(allocator);
self.ge_orders.deinit(allocator);
}
pub fn clone(self: *Store, allocator: std.mem.Allocator) !Store {
var items = try self.items.clone(allocator);
errdefer items.deinit(allocator);
var characters = try self.characters.clone(allocator);
errdefer characters.deinit(allocator);
var tasks = try self.tasks.clone(allocator);
errdefer tasks.deinit(allocator);
var monsters = try self.monsters.clone(allocator);
errdefer monsters.deinit(allocator);
var resources = try self.resources.clone(allocator);
errdefer resources.deinit(allocator);
var ge_orders = try self.ge_orders.clone(allocator);
errdefer ge_orders.deinit(allocator);
var maps = try self.maps.clone(allocator);
errdefer maps.deinit(allocator);
var images = try self.images.clone(allocator);
errdefer images.deinit(allocator);
return Store{
.items = items,
.characters = characters,
.tasks = tasks,
.monsters = monsters,
.resources = resources,
.ge_orders = ge_orders,
.maps = maps,
.bank = self.bank,
.images = images
};
}
const SaveData = struct {
api_version: []const u8,
codes: [][]u8,
items: []Item,
items: []Items.OptionalObject,
characters: []Characters.OptionalObject,
tasks: []Tasks.OptionalObject,
monsters: []Monsters.OptionalObject,
resources: []Resources.OptionalObject,
maps: []Map,
resources: []Resource,
monsters: []Monster,
images: struct {
rgba_data: []u8,
images: []Image,
// These next fields are just `category_mapping`
character_mapping: []Id,
item_mapping: []Id,
monster_mapping: []Id,
map_mapping: []Id,
resource_mapping: []Id,
effect_mapping: []Id,
}
};
fn allocHashMapValues(Value: type, allocator: Allocator, hashmap: anytype) ![]Value {
var values = try allocator.alloc(Value, hashmap.count());
errdefer allocator.free(values);
var valueIter = hashmap.valueIterator();
var index: usize = 0;
while (valueIter.next()) |value| {
values[index] = value.*;
index += 1;
}
return values;
}
// TODO: Create a better serialized that doesn't write zeros or BoundedArray types
pub fn save(self: *Store, api_version: []const u8, writer: anytype) !void {
const items = try allocHashMapValues(Item, self.allocator, self.items);
defer self.allocator.free(items);
const maps = try allocHashMapValues(Map, self.allocator, self.maps);
defer self.allocator.free(maps);
const resources = try allocHashMapValues(Resource, self.allocator, self.resources);
defer self.allocator.free(resources);
const monsters = try allocHashMapValues(Monster, self.allocator, self.monsters);
defer self.allocator.free(monsters);
const rgba_end_index = self.images.rgba_fba.end_index;
const data = SaveData{
.api_version = api_version,
.codes = self.codes.items,
.items = items,
.maps = maps,
.resources = resources,
.monsters = monsters
.items = self.items.objects.items,
.characters = self.characters.objects.items,
.tasks = self.tasks.objects.items,
.resources = self.resources.objects.items,
.monsters = self.monsters.objects.items,
.maps = self.maps.items,
.images = .{
.rgba_data = self.images.rgba_data[0..rgba_end_index],
.images = self.images.images.items,
.character_mapping = self.images.category_mapping.get(.character).items,
.item_mapping = self.images.category_mapping.get(.item).items,
.monster_mapping = self.images.category_mapping.get(.monster).items,
.map_mapping = self.images.category_mapping.get(.map).items,
.resource_mapping = self.images.category_mapping.get(.resource).items,
.effect_mapping = self.images.category_mapping.get(.effect).items,
}
};
try s2s.serialize(writer, SaveData, data);
}
pub fn load(self: *Store, api_version: []const u8, reader: anytype) !void {
var data = try s2s.deserializeAlloc(reader, SaveData, self.allocator);
pub fn load(self: *Store, allocator: std.mem.Allocator, api_version: []const u8, reader: anytype) !void {
var data: SaveData = try s2s.deserializeAlloc(reader, SaveData, allocator);
defer s2s.free(allocator, SaveData, &data);
// TODO: Add better version checking.
// * Hash the layout of `SaveData` and save that hash into the file. To see if layout changed.
// * Hash the openapi documentation file to see if the saved data is out-of-date and should be ignored.
if (!std.mem.eql(u8, data.api_version, api_version)) {
s2s.free(self.allocator, SaveData, &data);
return error.InvalidVersion;
}
defer self.allocator.free(data.api_version);
self.clearCodes();
try self.codes.appendSlice(data.codes);
defer self.allocator.free(data.codes);
const repositories = .{
.{ &self.items, data.items },
.{ &self.characters, data.characters },
.{ &self.tasks, data.tasks },
.{ &self.resources, data.resources },
.{ &self.monsters, data.monsters },
};
self.clearItems();
for (data.items) |item| {
try self.putItem(item);
}
defer self.allocator.free(data.items);
const image_category_mapping = &self.images.category_mapping;
const image_categories = .{
.{ image_category_mapping.getPtr(.character), data.images.character_mapping },
.{ image_category_mapping.getPtr(.item), data.images.item_mapping },
.{ image_category_mapping.getPtr(.monster), data.images.monster_mapping },
.{ image_category_mapping.getPtr(.map), data.images.map_mapping },
.{ image_category_mapping.getPtr(.resource), data.images.resource_mapping },
.{ image_category_mapping.getPtr(.effect), data.images.effect_mapping },
};
self.clearMaps();
for (data.maps) |map| {
try self.putMap(map);
}
defer self.allocator.free(data.maps);
// Check if there is enough space
{
inline for (repositories) |pair| {
const repository = pair[0];
const saved_objects = pair[1];
if (saved_objects.len > repository.objects.capacity) {
return error.OutOfMemory;
}
}
self.clearResources();
for (data.resources) |resource| {
try self.putResource(resource);
}
defer self.allocator.free(data.resources);
if (data.maps.len > self.maps.capacity) {
return error.OutOfMemory;
}
self.clearMonsters();
for (data.monsters) |monster| {
try self.putMonster(monster);
}
defer self.allocator.free(data.monsters);
}
if (data.images.rgba_data.len > self.images.rgba_data.len) {
return error.OutOfMemory;
}
// ----------------------- Character ------------------------------
if (data.images.images.len > self.images.images.capacity) {
return error.OutOfMemory;
}
fn getCharacterIndex(self: *const Store, name: []const u8) ?usize {
for (0.., self.characters.items) |i, character| {
if (std.mem.eql(u8, character.name, name)) {
return i;
inline for (image_categories) |pair| {
const self_ids = pair[0];
const saved_ids = pair[1];
if (saved_ids.len > self_ids.capacity) {
return error.OutOfMemory;
}
}
}
return null;
}
// Move loaded data from file to this store
{
inline for (repositories) |pair| {
const repository = pair[0];
const saved_objects = pair[1];
pub fn getCharacter(self: *Store, name: []const u8) ?Character {
if (self.getCharacterIndex(name)) |index| {
return self.characters.items[index];
repository.objects.clearRetainingCapacity();
repository.objects.appendSliceAssumeCapacity(saved_objects);
}
self.maps.clearRetainingCapacity();
self.maps.appendSliceAssumeCapacity(data.maps);
@memcpy(self.images.rgba_data[0..data.images.rgba_data.len], data.images.rgba_data);
self.images.rgba_fba.end_index = data.images.rgba_data.len;
self.images.images.clearRetainingCapacity();
self.images.images.appendSliceAssumeCapacity(data.images.images);
inline for (image_categories) |pair| {
const self_ids = pair[0];
const saved_ids = pair[1];
self_ids.clearRetainingCapacity();
self_ids.appendSliceAssumeCapacity(saved_ids);
}
}
return null;
}
pub fn putCharacter(self: *Store, character: Character) !void {
if (self.getCharacterIndex(character.name)) |index| {
self.characters.items[index].deinit();
self.characters.items[index] = character;
pub fn appendOrUpdateMap(self: *Store, map: Map) !Position {
if (self.getMap(map.position)) |existing_map| {
existing_map.* = map;
} else {
try self.characters.append(character);
}
}
// ----------------------- Map ------------------------------
pub fn getMap(self: *Store, x: i64, y: i64) ?Map {
const pos = Position.init(x, y);
return self.maps.get(pos);
}
pub fn getMaps(self: *Store, opts: Server.MapOptions) !std.ArrayList(Map) {
var found = std.ArrayList(Map).init(self.allocator);
errdefer found.deinit();
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;
const map_content_code = self.getCode(map.content.?.code_id).?;
if (!std.mem.eql(u8, map_content_code, content_code)) continue;
if (self.maps.unusedCapacitySlice().len == 0) {
return error.OutOfMemory;
}
try found.append(map.*);
self.maps.appendAssumeCapacity(map);
}
return found;
return map.position;
}
pub fn putMap(self: *Store, map: Map) !void {
var entry = try self.maps.getOrPut(map.position);
if (entry.found_existing) {
entry.value_ptr.deinit(self.allocator);
}
entry.value_ptr.* = map;
}
fn clearMaps(self: *Store) void {
var mapsIter = self.maps.valueIterator();
while (mapsIter.next()) |map| {
map.deinit(self.allocator);
}
self.maps.clearAndFree();
}
// ----------------------- Item ------------------------------
pub fn getItem(self: *Store, code: []const u8) ?Item {
return self.items.get(code);
}
pub fn getItems(self: *Store, opts: Server.ItemOptions) !std.ArrayList(Item) {
var found = std.ArrayList(Item).init(self.allocator);
errdefer found.deinit();
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;
pub fn getMap(self: *Store, position: Position) ?*Map {
for (self.maps.items) |*map| {
if (map.position.eql(position)) {
return map;
}
if (opts.craft_material) |craft_material| {
if (item.craft == null) continue;
const recipe = item.craft.?;
const craft_material_id = try self.getCodeId(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;
}
pub fn putItem(self: *Store, item: Item) !void {
const code = self.getCode(item.code_id).?;
var entry = try self.items.getOrPut(code);
if (entry.found_existing) {
entry.value_ptr.deinit(self.allocator);
}
entry.value_ptr.* = item;
}
fn clearItems(self: *Store) void {
var itemsIter = self.items.valueIterator();
while (itemsIter.next()) |item| {
item.deinit(self.allocator);
}
self.items.clearAndFree();
}
// ----------------------- Monster ------------------------------
pub fn getMonster(self: *Store, code: []const u8) ?Monster {
return self.monsters.get(code);
}
pub fn getMonsters(self: *Store, opts: Server.MonsterOptions) !std.ArrayList(Monster) {
var found = std.ArrayList(Monster).init(self.allocator);
errdefer found.deinit();
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.getCodeId(drop);
if (!DropRate.doesListContain(&monster.drops, item_id)) {
continue;
}
}
try found.append(monster.*);
}
return found;
}
pub fn putMonster(self: *Store, monster: Monster) !void {
const code = self.getCode(monster.code_id).?;
var entry = try self.monsters.getOrPut(code);
if (entry.found_existing) {
entry.value_ptr.deinit(self.allocator);
}
entry.value_ptr.* = monster;
}
fn clearMonsters(self: *Store) void {
var monstersIter = self.monsters.valueIterator();
while (monstersIter.next()) |monster| {
monster.deinit(self.allocator);
}
self.monsters.clearAndFree();
}
// ----------------------- Resource ------------------------------
pub fn getResource(self: *Store, code: []const u8) ?Resource {
return self.resources.get(code);
}
pub fn getResources(self: *Store, opts: Server.ResourceOptions) !std.ArrayList(Resource) {
var found = std.ArrayList(Resource).init(self.allocator);
errdefer found.deinit();
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.getCodeId(drop);
if (!DropRate.doesListContain(&resource.drops, item_id)) {
continue;
}
}
try found.append(resource.*);
}
return found;
}
pub fn putResource(self: *Store, resource: Resource) !void {
const code = self.getCode(resource.code_id).?;
var entry = try self.resources.getOrPut(code);
if (entry.found_existing) {
entry.value_ptr.deinit(self.allocator);
}
entry.value_ptr.* = resource;
}
fn clearResources(self: *Store) void {
var resourcesIter = self.resources.valueIterator();
while (resourcesIter.next()) |resource| {
resource.deinit(self.allocator);
}
self.resources.clearAndFree();
return null;
}

View File

@ -22,6 +22,10 @@ pub fn build(b: *std.Build) void {
});
api.addIncludePath(b.path("api/date_time"));
api.addCSourceFile(.{ .file = b.path("api/date_time/timegm.c") });
api.addIncludePath(b.path("api/stb_image"));
api.addCSourceFile(.{ .file = b.path("api/stb_image/stb_image.c") });
api.addImport("s2s", s2s_dep.module("s2s"));
}
@ -82,8 +86,10 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
gui.root_module.addImport("artificer", lib);
gui.linkLibrary(raylib_dep.artifact("raylib"));
gui.root_module.addImport("artifacts-api", api);
gui.root_module.addImport("artificer", lib);
gui.root_module.addImport("raylib", raylib_dep.module("raylib"));
const run_cmd = b.addRunArtifact(gui);

View File

@ -1,3 +1,4 @@
// zig fmt: off
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
@ -5,6 +6,15 @@ const Allocator = std.mem.Allocator;
const Artificer = @import("artificer");
const Api = @import("artifacts-api");
const simulated = true;
pub const std_options = .{
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .api, .level = .info },
.{ .scope = .artificer, .level = .debug },
}
};
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
@ -18,7 +28,7 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
var token_buffer: [256]u8 = undefined;
const token = try cwd.readFile(filename, &token_buffer);
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
return try allocator.dupe(u8, std.mem.trim(u8, token, "\n\t "));
}
pub fn main() !void {
@ -29,25 +39,54 @@ pub fn main() !void {
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
var artificer = try Artificer.init(allocator, token);
defer artificer.deinit();
var store = try Api.Store.init(allocator);
defer store.deinit(allocator);
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
defer allocator.free(cache_path);
var server = try Api.Server.init(allocator, &store);
defer server.deinit();
try server.setToken(token);
std.log.info("Prefetching server data", .{});
try artificer.server.prefetchCached(cache_path);
{
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
defer allocator.free(cwd_path);
if (false) {
std.log.info("Starting main loop", .{});
while (true) {
const waitUntil = artificer.nextStepAt();
const duration = waitUntil - std.time.milliTimestamp();
if (duration > 0) {
std.time.sleep(@intCast(duration));
const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store-cli.bin" });
defer allocator.free(cache_path);
try server.prefetchCached(allocator, cache_path, .{ .images = false });
}
const character_id = (try server.getCharacter("Blondie")).?;
var system_clock = Artificer.SystemClock{};
var sim_server = Artificer.SimServer.init(0, &store);
if (simulated) {
const character = store.characters.get(character_id).?;
character.cooldown_expiration = null;
}
var artificer = if (simulated)
try Artificer.ArtificerSim.init(allocator, &store, &sim_server.clock, &sim_server, character_id)
else
try Artificer.ArtificerApi.init(allocator, &store, &system_clock, &server, character_id);
defer artificer.deinit(allocator);
_ = try artificer.appendGoal(.{
.gather = .{
.item = store.items.getId("copper_ore").?,
.quantity = 3
}
});
try artificer.step();
}
}
std.log.info("Starting main loop", .{});
const started_at = artificer.clock.nanoTimestamp();
try artificer.runUntilGoalsComplete();
const stopped_at = artificer.clock.nanoTimestamp();
const elapsed_time = @as(f64, @floatFromInt(stopped_at - started_at)) / std.time.ns_per_s;
std.log.info("Took {d:.3}s", .{ elapsed_time });
}

698
gui/app.zig Normal file
View File

@ -0,0 +1,698 @@
// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const Artificer = @import("artificer");
const UI = @import("./ui.zig");
const RectUtils = @import("./rect-utils.zig");
const rl = @import("raylib");
const FontFace = @import("./font-face.zig");
const srcery = @import("./srcery.zig");
const rlgl_h = @cImport({
@cInclude("rlgl.h");
});
const assert = std.debug.assert;
const App = @This();
const MapTexture = struct {
name: Api.Map.Skin,
texture: rl.Texture2D,
};
ui: UI,
server: *Api.Server,
store: *Api.Store,
map_textures: std.ArrayList(MapTexture),
map_texture_indexes: std.ArrayList(usize),
map_position_min: Api.Position,
map_position_max: Api.Position,
camera: rl.Camera2D,
font_face: FontFace,
blur_texture_original: ?rl.RenderTexture = null,
blur_texture_horizontal: ?rl.RenderTexture = null,
blur_texture_both: ?rl.RenderTexture = null,
blur_shader: rl.Shader,
pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !App {
var map_textures = std.ArrayList(MapTexture).init(allocator);
errdefer map_textures.deinit();
errdefer {
for (map_textures.items) |map_texture| {
map_texture.texture.unload();
}
}
var map_texture_indexes = std.ArrayList(usize).init(allocator);
errdefer map_texture_indexes.deinit();
// Load all map textures from api store
{
const map_image_ids = store.images.category_mapping.get(.map).items;
try map_textures.ensureTotalCapacity(map_image_ids.len);
for (map_image_ids) |image_id| {
const image = store.images.get(image_id).?;
const texture = rl.loadTextureFromImage(rl.Image{
.width = @intCast(image.width),
.height = @intCast(image.height),
.data = store.images.getRGBA(image_id).?.ptr,
.mipmaps = 1,
.format = rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8
});
if (!rl.isTextureReady(texture)) {
return error.LoadMapTextureFromImage;
}
map_textures.appendAssumeCapacity(MapTexture{
.name = try Api.Map.Skin.fromSlice(image.code.slice()),
.texture = texture
});
}
}
var map_position_max = Api.Position.zero();
var map_position_min = Api.Position.zero();
for (store.maps.items) |map| {
map_position_min.x = @min(map_position_min.x, map.position.x);
map_position_min.y = @min(map_position_min.y, map.position.y);
map_position_max.x = @max(map_position_max.x, map.position.x);
map_position_max.y = @max(map_position_max.y, map.position.y);
}
const map_size = map_position_max.subtract(map_position_min);
try map_texture_indexes.ensureTotalCapacity(@intCast(map_size.x * map_size.y));
for (0..@intCast(map_size.y)) |oy| {
for (0..@intCast(map_size.x)) |ox| {
const x = map_position_min.x + @as(i64, @intCast(ox));
const y = map_position_min.y + @as(i64, @intCast(oy));
const map = store.getMap(.{ .x = x, .y = y }).?;
var found_texture = false;
const map_skin = map.skin.slice();
for (0.., map_textures.items) |i, map_texture| {
if (std.mem.eql(u8, map_skin, map_texture.name.slice())) {
map_texture_indexes.appendAssumeCapacity(i);
found_texture = true;
break;
}
}
if (!found_texture) {
return error.MapImageNotFound;
}
}
}
const blur_shader = rl.loadShaderFromMemory(
@embedFile("./base.vsh"),
@embedFile("./blur.fsh"),
);
if (!rl.isShaderReady(blur_shader)) {
return error.LoadShaderFromMemory;
}
var fontChars: [95]i32 = undefined;
for (0..fontChars.len) |i| {
fontChars[i] = 32 + @as(i32, @intCast(i));
}
var font = rl.loadFontFromMemory(".ttf", @embedFile("./roboto-font/Roboto-Medium.ttf"), 16, &fontChars);
if (!font.isReady()) {
return error.LoadFontFromMemory;
}
return App{
.store = store,
.server = server,
.ui = UI.init(),
.map_textures = map_textures,
.map_texture_indexes = map_texture_indexes,
.map_position_max = map_position_max,
.map_position_min = map_position_min,
.blur_shader = blur_shader,
.font_face = .{ .font = font },
.camera = rl.Camera2D{
.offset = rl.Vector2.zero(),
.target = rl.Vector2.zero(),
.rotation = 0,
.zoom = 1,
}
};
}
pub fn deinit(self: *App) void {
for (self.map_textures.items) |map_texture| {
map_texture.texture.unload();
}
self.map_textures.deinit();
self.map_texture_indexes.deinit();
self.font_face.font.unload();
if (self.blur_texture_horizontal) |render_texture| {
render_texture.unload();
}
if (self.blur_texture_both) |render_texture| {
render_texture.unload();
}
if (self.blur_texture_original) |render_texture| {
render_texture.unload();
}
}
fn cameraControls(camera: *rl.Camera2D) void {
if (rl.isMouseButtonDown(.mouse_button_left)) {
const mouse_delta = rl.getMouseDelta();
camera.target.x -= mouse_delta.x / camera.zoom;
camera.target.y -= mouse_delta.y / camera.zoom;
}
const zoom_speed = 0.2;
const min_zoom = 0.1;
const max_zoom = 2;
const zoom_delta = rl.getMouseWheelMove();
if (zoom_delta != 0) {
const mouse_screen = rl.getMousePosition();
// Get the world point that is under the mouse
const mouse_world = rl.getScreenToWorld2D(mouse_screen, camera.*);
// Set the offset to where the mouse is
camera.offset = mouse_screen;
// Set the target to match, so that the camera maps the world space point
// under the cursor to the screen space point under the cursor at any zoom
camera.target = mouse_world;
// Zoom increment
camera.zoom *= (1 + zoom_delta * zoom_speed);
camera.zoom = std.math.clamp(camera.zoom, min_zoom, max_zoom);
}
}
// fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void {
// var buffer: [256]u8 = undefined;
//
// const name_height = 20;
// UI.drawTextCentered(ui.font, brain.name, .{ .x = RectUtils.center(rect).x, .y = rect.y + name_height / 2 }, 20, 2, srcery.bright_white);
//
// var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom);
// label_stack.gap = 4;
//
// const now = std.time.milliTimestamp();
// const cooldown = brain.cooldown(&artificer.server);
// const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s;
// const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{seconds_left});
// UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
//
// var task_label: []u8 = undefined;
// if (brain.task) |task| {
// task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{@tagName(task)});
// } else {
// task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{});
// }
// UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
//
// const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{brain.action_queue.items.len});
// UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
// }
fn createOrGetRenderTexture(maybe_render_texture: *?rl.RenderTexture) !rl.RenderTexture {
const screen_width = rl.getScreenWidth();
const screen_height = rl.getScreenHeight();
if (maybe_render_texture.*) |render_texture| {
if (render_texture.texture.width != screen_width or render_texture.texture.height != screen_height) {
render_texture.unload();
maybe_render_texture.* = null;
}
}
if (maybe_render_texture.* == null) {
const render_texture = rl.loadRenderTexture(screen_width, screen_height);
if (!rl.isRenderTextureReady(render_texture)) {
return error.LoadRenderTexture;
}
maybe_render_texture.* = render_texture;
}
return maybe_render_texture.*.?;
}
// Modified version of `DrawRectangleRounded` where the UV texture coordiantes are consistent and align
fn drawRectangleRoundedUV(rec: rl.Rectangle, roundness: f32, color: rl.Color) void {
assert(roundness < 1);
if (roundness <= 0 or rec.width <= 1 or rec.height <= 1) {
rl.drawRectangleRec(rec, color);
return;
}
const radius: f32 = @min(rec.width, rec.height) * roundness / 2;
if (radius <= 0.0) return;
// Calculate the maximum angle between segments based on the error rate (usually 0.5f)
const smooth_circle_error_rate = 0.5;
const th: f32 = std.math.acos(2 * std.math.pow(f32, 1 - smooth_circle_error_rate / radius, 2) - 1);
var segments: i32 = @intFromFloat(@ceil(2 * std.math.pi / th) / 4.0);
if (segments <= 0) segments = 4;
const step_length = 90.0 / @as(f32, @floatFromInt(segments));
// Quick sketch to make sense of all of this,
// there are 9 parts to draw, also mark the 12 points we'll use
//
// P0____________________P1
// /| |\
// /1| 2 |3\
// P7 /__|____________________|__\ P2
// | |P8 P9| |
// | 8 | 9 | 4 |
// | __|____________________|__ |
// P6 \ |P11 P10| / P3
// \7| 6 |5/
// \|____________________|/
// P5 P4
// Coordinates of the 12 points that define the rounded rect
const radius_u = radius / rec.width;
const radius_v = radius / rec.height;
const points = [_]rl.Vector2{
.{ .x = radius_u , .y = 0 }, // P0
.{ .x = 1 - radius_u , .y = 0 }, // P1
.{ .x = 1 , .y = radius_v }, // P2
.{ .x = 1 , .y = 1 - radius_v }, // P3
.{ .x = 1 - radius_u , .y = 1 }, // P4
.{ .x = radius_u , .y = 1 }, // P5
.{ .x = 0 , .y = 1 - radius_v }, // P6
.{ .x = 0 , .y = radius_v }, // P7
.{ .x = radius_u , .y = radius_v }, // P8
.{ .x = 1 - radius_u , .y = radius_v }, // P9
.{ .x = 1 - radius_u , .y = 1 - radius_v }, // P10
.{ .x = radius_u , .y = 1 - radius_v }, // P11
};
const texture = rl.getShapesTexture();
const shape_rect = rl.getShapesTextureRectangle();
const texture_width: f32 = @floatFromInt(texture.width);
const texture_height: f32 = @floatFromInt(texture.height);
rl.gl.rlBegin(rlgl_h.RL_TRIANGLES);
defer rl.gl.rlEnd();
rl.gl.rlSetTexture(texture.id);
defer rl.gl.rlSetTexture(0);
// Draw all of the 4 corners: [1] Upper Left Corner, [3] Upper Right Corner, [5] Lower Right Corner, [7] Lower Left Corner
const centers = [_]rl.Vector2{ points[8], points[9], points[10], points[11] };
const angles = [_]f32{ 180.0, 270.0, 0.0, 90.0 };
for (0..4) |k| {
var angle = angles[k];
const center = centers[k];
for (0..@intCast(segments)) |_| {
rl.gl.rlColor4ub(color.r, color.g, color.b, color.a);
const rad_per_deg = std.math.rad_per_deg;
const triangle = .{
center,
.{
.x = center.x + @cos(rad_per_deg*(angle + step_length))*radius_u,
.y = center.y + @sin(rad_per_deg*(angle + step_length))*radius_v
},
.{
.x = center.x + @cos(rad_per_deg * angle)*radius_u,
.y = center.y + @sin(rad_per_deg * angle)*radius_v
}
};
inline for (triangle) |point| {
rl.gl.rlTexCoord2f(
(shape_rect.x + shape_rect.width * point.x) / texture_width,
(shape_rect.y + shape_rect.height * point.y) / texture_height
);
rl.gl.rlVertex2f(
rec.x + rec.width * point.x,
rec.y + rec.height * point.y
);
}
angle += step_length;
}
}
// [2] Upper Rectangle
rl.gl.rlColor4ub(color.r, color.g, color.b, color.a);
inline for (.{ 0, 8, 9, 1, 0, 9 }) |index| {
const point = points[index];
rl.gl.rlTexCoord2f(
(shape_rect.x + shape_rect.width * point.x) / texture_width,
(shape_rect.y + shape_rect.height * point.y) / texture_height
);
rl.gl.rlVertex2f(
rec.x + rec.width * point.x,
rec.y + rec.height * point.y
);
}
// [4] Right Rectangle
rl.gl.rlColor4ub(color.r, color.g, color.b, color.a);
inline for (.{ 9, 10, 3, 2, 9, 3 }) |index| {
const point = points[index];
rl.gl.rlTexCoord2f(
(shape_rect.x + shape_rect.width * point.x) / texture_width,
(shape_rect.y + shape_rect.height * point.y) / texture_height
);
rl.gl.rlVertex2f(
rec.x + rec.width * point.x,
rec.y + rec.height * point.y
);
}
// [6] Bottom Rectangle
rl.gl.rlColor4ub(color.r, color.g, color.b, color.a);
inline for (.{ 11, 5, 4, 10, 11, 4 }) |index| {
const point = points[index];
rl.gl.rlTexCoord2f(
(shape_rect.x + shape_rect.width * point.x) / texture_width,
(shape_rect.y + shape_rect.height * point.y) / texture_height
);
rl.gl.rlVertex2f(
rec.x + rec.width * point.x,
rec.y + rec.height * point.y
);
}
// [8] Left Rectangle
rl.gl.rlColor4ub(color.r, color.g, color.b, color.a);
inline for (.{ 7, 6, 11, 8, 7, 11 }) |index| {
const point = points[index];
rl.gl.rlTexCoord2f(
(shape_rect.x + shape_rect.width * point.x) / texture_width,
(shape_rect.y + shape_rect.height * point.y) / texture_height
);
rl.gl.rlVertex2f(
rec.x + rec.width * point.x,
rec.y + rec.height * point.y
);
}
// [9] Middle Rectangle
rl.gl.rlColor4ub(color.r, color.g, color.b, color.a);
inline for (.{ 8, 11, 10, 9, 8, 10 }) |index| {
const point = points[index];
rl.gl.rlTexCoord2f(
(shape_rect.x + shape_rect.width * point.x) / texture_width,
(shape_rect.y + shape_rect.height * point.y) / texture_height
);
rl.gl.rlVertex2f(
rec.x + rec.width * point.x,
rec.y + rec.height * point.y
);
}
}
fn drawBlurredWorld(self: *App, rect: rl.Rectangle, color: rl.Color) void {
const blur_both = self.blur_texture_both.?.texture;
const previous_texture = rl.getShapesTexture();
const previous_rect = rl.getShapesTextureRectangle();
defer rl.setShapesTexture(previous_texture, previous_rect);
const texture_height: f32 = @floatFromInt(blur_both.height);
const shape_rect = rl.Rectangle{
.x = rect.x,
.y = texture_height - rect.y,
.width = rect.width,
.height = -rect.height,
};
rl.setShapesTexture(blur_both, shape_rect);
const border = 2;
const roundness = 0.2;
drawRectangleRoundedUV(rect, roundness, color);
rl.drawRectangleRoundedLinesEx(RectUtils.shrink(rect, border - 1, border - 1), roundness, 0, border, srcery.bright_white.alpha(0.3));
}
pub fn drawWorld(self: *App) void {
rl.clearBackground(srcery.black);
rl.drawCircleV(rl.Vector2.zero(), 5, rl.Color.red);
const map_size = self.map_position_max.subtract(self.map_position_min);
for (0..@intCast(map_size.y)) |oy| {
for (0..@intCast(map_size.x)) |ox| {
const map_index = @as(usize, @intCast(map_size.x)) * oy + ox;
const x = self.map_position_min.x + @as(i64, @intCast(ox));
const y = self.map_position_min.y + @as(i64, @intCast(oy));
const texture_index = self.map_texture_indexes.items[map_index];
const texture = self.map_textures.items[texture_index].texture;
const tile_size = rl.Vector2.init(224, 224);
const position = rl.Vector2.init(@floatFromInt(x), @floatFromInt(y)).multiply(tile_size);
rl.drawTextureV(texture, position, rl.Color.white);
}
}
}
pub fn drawWorldAndBlur(self: *App) !void {
const screen_width = rl.getScreenWidth();
const screen_height = rl.getScreenHeight();
const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height));
const blur_original = try createOrGetRenderTexture(&self.blur_texture_original);
const blur_horizontal = try createOrGetRenderTexture(&self.blur_texture_horizontal);
const blur_both = try createOrGetRenderTexture(&self.blur_texture_both);
// 1 pass. Draw the all of the sprites
{
blur_original.begin();
defer blur_original.end();
self.camera.begin();
defer self.camera.end();
self.drawWorld();
}
// 2 pass. Apply horizontal blur
const kernel_radius: i32 = 16;
var kernel_coeffs: [kernel_radius * 2 + 1]f32 = undefined;
{
const sigma = 10;
for (0..kernel_coeffs.len) |i| {
const i_i32: i32 = @intCast(i);
const io: f32 = @floatFromInt(i_i32 - kernel_radius);
kernel_coeffs[i] = @exp(-(io * io) / (sigma * sigma));
// kernel_coeffs[i] /= @floatFromInt(kernel_coeffs.len);
}
var kernel_sum: f32 = 0;
for (kernel_coeffs) |coeff| {
kernel_sum += coeff;
}
for (&kernel_coeffs) |*coeff| {
coeff.* /= kernel_sum;
}
}
{
const texture_size_loc = rl.getShaderLocation(self.blur_shader, "textureSize");
assert(texture_size_loc != -1);
rl.setShaderValue(self.blur_shader, texture_size_loc, &screen_size, .shader_uniform_vec2);
}
{
const kernel_radius_loc = rl.getShaderLocation(self.blur_shader, "kernelRadius");
assert(kernel_radius_loc != -1);
rl.setShaderValue(self.blur_shader, kernel_radius_loc, &kernel_radius, .shader_uniform_int);
}
{
const coeffs_loc = rl.getShaderLocation(self.blur_shader, "coeffs");
assert(coeffs_loc != -1);
rl.setShaderValueV(self.blur_shader, coeffs_loc, &kernel_coeffs, .shader_uniform_float, kernel_coeffs.len);
}
{
const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection");
assert(kernel_direction_loc != -1);
const kernel_direction = rl.Vector2.init(1, 0);
rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2);
}
{
blur_horizontal.begin();
defer blur_horizontal.end();
rl.clearBackground(rl.Color.black.alpha(0));
self.blur_shader.activate();
defer self.blur_shader.deactivate();
rl.drawTextureRec(
blur_original.texture,
.{
.x = 0,
.y = 0,
.width = screen_size.x,
.height = -screen_size.y,
},
rl.Vector2.zero(),
rl.Color.white
);
}
// 3 pass. Apply vertical blur
{
const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection");
assert(kernel_direction_loc != -1);
const kernel_direction = rl.Vector2.init(0, 1);
rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2);
}
{
blur_both.begin();
defer blur_both.end();
self.blur_shader.activate();
defer self.blur_shader.deactivate();
rl.drawTextureRec(
blur_horizontal.texture,
.{
.x = 0,
.y = 0,
.width = screen_size.x,
.height = -screen_size.y,
},
rl.Vector2.zero(),
rl.Color.white
);
}
// Last thing, draw world without blur
rl.drawTextureRec(
blur_original.texture,
.{
.x = 0,
.y = 0,
.width = @floatFromInt(blur_original.texture.width),
.height = @floatFromInt(-blur_original.texture.height),
},
rl.Vector2.zero(),
rl.Color.white
);
}
fn drawRatelimits(self: *App, box: rl.Rectangle) void {
const Category = Api.RateLimit.Category;
const ratelimits = self.server.ratelimits;
self.drawBlurredWorld(
box,
srcery.xgray10
);
const padding = 16;
var stack = UI.Stack.init(RectUtils.shrink(box, padding, padding), .top_to_bottom);
stack.gap = 8;
inline for (.{
.{ "Account creation", Category.account_creation },
.{ "Token", Category.token },
.{ "Data", Category.data },
.{ "Actions", Category.actions },
}) |ratelimit_bar| {
const title = ratelimit_bar[0];
const category = ratelimit_bar[1];
const ratelimit = ratelimits.get(category);
const ratelimit_box = stack.next(24);
rl.drawRectangleRec(ratelimit_box, rl.Color.white);
inline for (.{
.{ ratelimit.hours , std.time.ms_per_hour, srcery.red },
.{ ratelimit.minutes, std.time.ms_per_min , srcery.blue },
.{ ratelimit.seconds, std.time.ms_per_s , srcery.green },
}) |limit_spec| {
const maybe_timespan = limit_spec[0];
const timespan_size = limit_spec[1];
const color = limit_spec[2];
if (maybe_timespan) |timespan| {
const limit_f32: f32 = @floatFromInt(timespan.limit);
const counter_f32: f32 = @floatFromInt(timespan.counter);
const timer_f32: f32 = @floatFromInt(timespan.timer_ms);
const ms_per_request = timespan_size / limit_f32;
const progress = std.math.clamp((counter_f32 - timer_f32 / ms_per_request) / limit_f32, 0, 1);
var progress_bar = ratelimit_box;
progress_bar.width *= progress;
rl.drawRectangleRec(progress_bar, color);
}
}
if (self.ui.isMouseInside(ratelimit_box)) {
// TODO: Draw more detailed info about rate limits.
// Show how many requests have occured
} else {
const title_size = self.font_face.measureText(title);
self.font_face.drawText(
title,
.{
.x = ratelimit_box.x + 8,
.y = ratelimit_box.y + title_size.y/2,
},
srcery.white
);
}
}
}
pub fn tick(self: *App) !void {
for (&self.server.ratelimits.values) |*ratelimit| {
ratelimit.update_timers(std.time.milliTimestamp());
}
const screen_width = rl.getScreenWidth();
const screen_height = rl.getScreenHeight();
const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height));
cameraControls(&self.camera);
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(srcery.black);
try self.drawWorldAndBlur();
self.drawRatelimits(
.{ .x = 20, .y = 20, .width = 200, .height = 200 },
);
rl.drawFPS(
@as(i32, @intFromFloat(screen_size.x)) - 100,
@as(i32, @intFromFloat(screen_size.y)) - 24
);
// var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
// for (artificer.characters.items) |*brain| {
// const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len));
// try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
// }
}

24
gui/base.vsh Normal file
View File

@ -0,0 +1,24 @@
#version 330
// Input vertex attributes
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
// Input uniform values
uniform mat4 mvp;
// Output vertex attributes (to fragment shader)
out vec2 fragTexCoord;
out vec4 fragColor;
void main()
{
// Send vertex attributes to fragment shader
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
// Calculate final vertex position
gl_Position = mvp * vec4(vertexPosition, 1.0);
}

45
gui/blur.fsh Normal file
View File

@ -0,0 +1,45 @@
#version 330
#define MAX_KERNEL_RADIUS 16
#define MAX_KERNEL_COEFFS 2*MAX_KERNEL_RADIUS + 1
// Input vertex attributes (from vertex shader)
in vec2 fragTexCoord;
in vec4 fragColor;
// Input uniform values
uniform sampler2D texture0;
uniform vec4 colDiffuse;
uniform vec2 textureSize;
uniform float coeffs[MAX_KERNEL_COEFFS];
uniform int kernelRadius;
uniform vec2 kernelDirection;
// Output fragment color
out vec4 finalColor;
void main()
{
vec2 texel = 1.0 / textureSize;
vec4 texelColor = vec4(0);
float alphaCorrection = 0;
for (int i = 0; i < 2*kernelRadius + 1; i++)
{
vec2 offset = kernelDirection * vec2(i - kernelRadius, i - kernelRadius) * texel;
vec2 sampleCoord = fragTexCoord + offset;
if ((0 <= sampleCoord.x && sampleCoord.x <= 1) && (0 <= sampleCoord.y && sampleCoord.y <= 1)) {
vec4 sample = texture(texture0, sampleCoord);
texelColor += sample * coeffs[i];
alphaCorrection += sample.a * coeffs[i];
}
}
texelColor /= alphaCorrection;
finalColor = texelColor * colDiffuse * fragColor;
}

138
gui/font-face.zig Normal file
View File

@ -0,0 +1,138 @@
const std = @import("std");
const rl = @import("raylib");
font: rl.Font,
spacing: ?f32 = null,
line_height: f32 = 1.4,
pub fn getSpacing(self: @This()) f32 {
if (self.spacing) |spacing| {
return spacing;
} else {
return self.getSize() / 10;
}
}
pub fn getSize(self: @This()) f32 {
return @floatFromInt(self.font.baseSize);
}
pub fn drawTextLines(self: @This(), lines: []const []const u8, position: rl.Vector2, tint: rl.Color) void {
var offset_y: f32 = 0;
const font_size = self.getSize();
for (lines) |line| {
self.drawText(line, position.add(.{ .x = 0, .y = offset_y }), tint);
const line_size = self.measureText(line);
offset_y += line_size.y + font_size * (self.line_height - 1);
}
}
pub fn measureTextLines(self: @This(), lines: []const []const u8) rl.Vector2 {
var text_size = rl.Vector2.zero();
const font_size = self.getSize();
for (lines) |line| {
const line_size = self.measureText(line);
text_size.x = @max(text_size.x, line_size.x);
text_size.y += line_size.y;
}
text_size.y += (self.line_height - 1) * font_size * @as(f32, @floatFromInt(@max(lines.len - 1, 0)));
return text_size;
}
pub fn drawTextEx(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color, offset: *rl.Vector2) void {
if (self.font.texture.id == 0) return;
const font_size = self.getSize();
const spacing = self.getSpacing();
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
if (codepoint == '\n') {
offset.x = 0;
offset.y += font_size * self.line_height;
} else {
if (!(codepoint <= 255 and std.ascii.isWhitespace(@intCast(codepoint)))) {
var codepoint_position = position.add(offset.*);
codepoint_position.x = @round(codepoint_position.x);
codepoint_position.y = @round(codepoint_position.y);
rl.drawTextCodepoint(self.font, codepoint, codepoint_position, font_size, tint);
}
const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint));
if (self.font.glyphs[index].advanceX != 0) {
offset.x += @floatFromInt(self.font.glyphs[index].advanceX);
} else {
offset.x += self.font.recs[index].width;
offset.x += @floatFromInt(self.font.glyphs[index].offsetX);
}
offset.x += spacing;
}
}
}
pub fn drawText(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void {
var offset = rl.Vector2.init(0, 0);
self.drawTextEx(text, position, tint, &offset);
}
pub fn drawTextAlloc(self: @This(), allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype, position: rl.Vector2, tint: rl.Color) !void {
const text = try std.fmt.allocPrint(allocator, fmt, args);
defer allocator.free(text);
self.drawText(text, position, tint);
}
pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
var text_size = rl.Vector2.zero();
if (self.font.texture.id == 0) return text_size; // Security check
if (text.len == 0) return text_size;
const font_size = self.getSize();
const spacing = self.getSpacing();
var line_width: f32 = 0;
text_size.y = font_size;
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
if (codepoint == '\n') {
text_size.y += font_size * self.line_height;
line_width = 0;
} else {
if (line_width > 0) {
line_width += spacing;
}
const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint));
if (self.font.glyphs[index].advanceX != 0) {
line_width += @floatFromInt(self.font.glyphs[index].advanceX);
} else {
line_width += self.font.recs[index].width;
line_width += @floatFromInt(self.font.glyphs[index].offsetX);
}
text_size.x = @max(text_size.x, line_width);
}
}
return text_size;
}
pub fn drawTextCenter(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void {
const text_size = self.measureText(text);
const adjusted_position = rl.Vector2{
.x = position.x - text_size.x / 2,
.y = position.y - text_size.y / 2,
};
self.drawText(text, adjusted_position, tint);
}

View File

@ -1,15 +1,64 @@
// zig fmt: off
const std = @import("std");
const Artificer = @import("artificer");
const Api = @import("artifacts-api");
const rl = @import("raylib");
const Allocator = std.mem.Allocator;
const raylib_h = @cImport({
@cInclude("stdio.h");
@cInclude("raylib.h");
});
const App = @import("./app.zig");
const srcery = @import("./srcery.zig");
pub const std_options = .{
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .api, .level = .warn },
.{ .scope = .raylib, .level = .warn },
}
};
const UI = @import("./ui.zig");
const UIStack = @import("./ui_stack.zig");
const RectUtils = @import("./rect_utils.zig");
fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
return switch (log_level) {
.err => rl.TraceLogLevel.log_error,
.warn => rl.TraceLogLevel.log_warning,
.info => rl.TraceLogLevel.log_info,
.debug => rl.TraceLogLevel.log_trace,
};
}
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
fn toZigLogLevel(log_type: c_int) ?std.log.Level {
return switch (log_type) {
@intFromEnum(rl.TraceLogLevel.log_trace) => std.log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_debug) => std.log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_info) => std.log.Level.info,
@intFromEnum(rl.TraceLogLevel.log_warning) => std.log.Level.warn,
@intFromEnum(rl.TraceLogLevel.log_error) => std.log.Level.err,
@intFromEnum(rl.TraceLogLevel.log_fatal) => std.log.Level.err,
else => null,
};
}
fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: [*c]raylib_h.struct___va_list_tag_1) callconv(.C) void {
const log_level = toZigLogLevel(logType) orelse return;
const scope = .raylib;
const raylib_log = std.log.scoped(scope);
const max_tracelog_msg_length = 256; // from utils.c in raylib
var buffer: [max_tracelog_msg_length:0]u8 = undefined;
inline for (std.meta.fields(std.log.Level)) |field| {
const message_level: std.log.Level = @enumFromInt(field.value);
if (std.log.logEnabled(message_level, scope) and log_level == message_level) {
@memset(&buffer, 0);
const text_length = raylib_h.vsnprintf(&buffer, buffer.len, text, args);
const formatted_text = buffer[0..@intCast(text_length)];
const log_function = @field(raylib_log, field.name);
@call(.auto, log_function, .{ "{s}", .{formatted_text} });
}
}
}
fn getAPITokenFromArgs(allocator: std.mem.Allocator) !?[]u8 {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
@ -22,37 +71,7 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
var token_buffer: [256]u8 = undefined;
const token = try cwd.readFile(filename, &token_buffer);
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
}
fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void {
var buffer: [256]u8 = undefined;
const name_height = 20;
UI.drawTextCentered(ui.font, brain.name, .{
.x = RectUtils.center(rect).x,
.y = rect.y + name_height/2
}, 20, 2, srcery.bright_white);
var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom);
label_stack.gap = 4;
const now = std.time.milliTimestamp();
const cooldown = brain.cooldown(&artificer.server);
const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s;
const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{ seconds_left });
UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
var task_label: []u8 = undefined;
if (brain.task) |task| {
task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{ @tagName(task) });
} else {
task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{ });
}
UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{ brain.action_queue.items.len });
UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
return try allocator.dupe(u8, std.mem.trim(u8, token, "\n\t "));
}
pub fn main() anyerror!void {
@ -60,45 +79,44 @@ pub fn main() anyerror!void {
defer _ = gpa.deinit();
const allocator = gpa.allocator();
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
rl.setTraceLogLevel(toRaylibTraceLogLevel(std.log.default_level));
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
var artificer = try Artificer.init(allocator, token);
defer artificer.deinit();
var store = try Api.Store.init(allocator);
defer store.deinit(allocator);
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
defer allocator.free(cache_path);
var server = try Api.Server.init(allocator, &store);
defer server.deinit();
try server.setToken(token);
std.log.info("Prefetching server data", .{});
try artificer.server.prefetchCached(cache_path);
{
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
defer allocator.free(cwd_path);
const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store-gui.bin" });
defer allocator.free(cache_path);
try server.prefetchCached(allocator, cache_path, .{ .images = true });
}
rl.initWindow(800, 450, "Artificer");
defer rl.closeWindow();
rl.setTargetFPS(60);
rl.setWindowMinSize(200, 200);
rl.setWindowState(.{
.vsync_hint = true,
.window_resizable = true
});
var ui = UI.init();
defer ui.deinit();
var app = try App.init(allocator, &store, &server);
defer app.deinit();
while (!rl.windowShouldClose()) {
if (std.time.milliTimestamp() > artificer.nextStepAt()) {
try artificer.step();
}
const screen_size = rl.Vector2.init(
@floatFromInt(rl.getScreenWidth()),
@floatFromInt(rl.getScreenHeight())
);
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(srcery.black);
var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
for (artificer.characters.items) |*brain| {
const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len));
try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
}
try app.tick();
}
}

202
gui/roboto-font/LICENSE.txt Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,159 +1,200 @@
const rl = @import("raylib");
const std = @import("std");
const rl = @import("raylib");
const rect_utils = @import("./rect-utils.zig");
const assert = std.debug.assert;
const SourceLocation = std.builtin.SourceLocation;
// TODO: Implement Id context (I.e. ID parenting)
const UI = @This();
font: rl.Font,
const max_stack_depth = 16;
const TransformFrame = struct {
offset: rl.Vector2,
scale: rl.Vector2,
};
hot_widget: ?Id = null,
active_widget: ?Id = null,
transform_stack: std.BoundedArray(TransformFrame, max_stack_depth),
pub fn init() UI {
var stack = std.BoundedArray(TransformFrame, max_stack_depth).init(0) catch unreachable;
stack.appendAssumeCapacity(TransformFrame{
.offset = rl.Vector2{ .x = 0, .y = 0 },
.scale = rl.Vector2{ .x = 1, .y = 1 },
});
return UI{
.font = rl.getFontDefault()
.transform_stack = stack
};
}
pub fn deinit(self: UI) void {
rl.unloadFont(self.font);
pub fn isHot(self: *const UI, id: Id) bool {
if (self.hot_widget) |hot_id| {
return hot_id.eql(id);
}
return false;
}
// Reimplementation of `GetGlyphIndex` from raylib in src/rtext.c
fn GetGlyphIndex(font: rl.Font, codepoint: i32) usize {
var index: usize = 0;
pub fn isActive(self: *const UI, id: Id) bool {
if (self.active_widget) |active_id| {
return active_id.eql(id);
}
return false;
}
var fallbackIndex: usize = 0; // Get index of fallback glyph '?'
pub fn hashSrc(src: SourceLocation) u64 {
var hash = std.hash.Fnv1a_64.init();
hash.update(src.file);
hash.update(std.mem.asBytes(&src.line));
hash.update(std.mem.asBytes(&src.column));
return hash.value;
}
for (0..@intCast(font.glyphCount), font.glyphs) |i, glyph| {
if (glyph.value == '?') fallbackIndex = i;
fn getTopFrame(self: *UI) *TransformFrame {
assert(self.transform_stack.len >= 1);
return &self.transform_stack.buffer[self.transform_stack.len-1];
}
if (glyph.value == codepoint)
{
index = i;
break;
}
pub fn getMousePosition(self: *UI) rl.Vector2 {
const frame = self.getTopFrame();
return rl.getMousePosition().subtract(frame.offset).divide(frame.scale);
}
pub fn getMouseDelta(self: *UI) rl.Vector2 {
const frame = self.getTopFrame();
return rl.Vector2.multiply(rl.getMouseDelta(), frame.scale);
}
pub fn getMouseWheelMove(self: *UI) f32 {
const frame = self.getTopFrame();
return rl.getMouseWheelMove() * frame.scale.y;
}
pub fn isMouseInside(self: *UI, rect: rl.Rectangle) bool {
return rect_utils.isInsideVec2(rect, self.getMousePosition());
}
pub fn transformScale(self: *UI, x: f32, y: f32) void {
const frame = self.getTopFrame();
frame.scale.x *= x;
frame.scale.y *= y;
rl.gl.rlScalef(x, y, 1);
}
pub fn transformTranslate(self: *UI, x: f32, y: f32) void {
const frame = self.getTopFrame();
frame.offset.x += x * frame.scale.x;
frame.offset.y += y * frame.scale.y;
rl.gl.rlTranslatef(x, y, 0);
}
pub fn pushTransform(self: *UI) void {
rl.gl.rlPushMatrix();
self.transform_stack.appendAssumeCapacity(self.getTopFrame().*);
}
pub fn popTransform(self: *UI) void {
assert(self.transform_stack.len >= 2);
rl.gl.rlPopMatrix();
_ = self.transform_stack.pop();
}
pub fn beginScissorMode(self: *UI, x: f32, y: f32, width: f32, height: f32) void {
const frame = self.getTopFrame();
rl.beginScissorMode(
@intFromFloat(x * frame.scale.x + frame.offset.x),
@intFromFloat(y * frame.scale.y + frame.offset.y),
@intFromFloat(width * frame.scale.x),
@intFromFloat(height * frame.scale.y),
);
}
pub fn beginScissorModeRect(self: *UI, rect: rl.Rectangle) void {
self.beginScissorMode(rect.x, rect.y, rect.width, rect.height);
}
pub fn endScissorMode(self: *UI) void {
_ = self;
rl.endScissorMode();
}
pub const Id = struct {
location: u64,
extra: u32 = 0,
pub fn init(comptime src: SourceLocation) Id {
return Id{ .location = comptime hashSrc(src) };
}
if ((index == 0) and (font.glyphs[0].value != codepoint)) index = fallbackIndex;
return index;
}
fn GetCodePointNext(text: []const u8, next: *usize) i32 {
var letter: i32 = '?';
if (std.unicode.utf8ByteSequenceLength(text[0])) |codepointSize| {
next.* = codepointSize;
if (std.unicode.utf8Decode(text[0..codepointSize])) |codepoint| {
letter = @intCast(codepoint);
} else |_| {}
} else |_| {}
return letter;
}
// NOTE: Line spacing is a global variable, use SetTextLineSpacing() to setup
const textLineSpacing = 2; // TODO: Assume that line spacing is not changed.
// Reimplementation of `rl.drawTextEx`, so a null terminated would not be required
pub fn drawTextEx(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, tint: rl.Color) void {
var used_font = font;
if (font.texture.id == 0) {
used_font = rl.getFontDefault();
pub fn eql(a: Id, b: Id) bool {
return a.location == b.location and a.extra == b.extra;
}
};
var text_offset_y: f32 = 0;
var text_offset_x: f32 = 0;
const scale_factor = font_size / @as(f32, @floatFromInt(used_font.baseSize));
var i: usize = 0;
while (i < text.len) {
var next: usize = 0;
const letter = GetCodePointNext(text[i..], &next);
const index = GetGlyphIndex(font, letter);
i += next;
if (letter == '\n') {
text_offset_x = 0;
text_offset_y += (font_size + textLineSpacing);
} else {
if (letter != ' ' and letter != '\t') {
rl.drawTextCodepoint(font, letter, .{
.x = position.x + text_offset_x,
.y = position.y + text_offset_y,
}, font_size, tint);
}
if (font.glyphs[index].advanceX == 0) {
text_offset_x += font.recs[index].width*scale_factor + spacing;
} else {
text_offset_x += @as(f32, @floatFromInt(font.glyphs[index].advanceX))*scale_factor + spacing;
}
}
}
}
// Reimplementation of `rl.measureTextEx`, so a null terminated would not be required
pub fn measureTextEx(font: rl.Font, text: []const u8, fontSize: f32, spacing: f32) rl.Vector2 {
var textSize = rl.Vector2.init(0, 0);
if (font.texture.id == 0) return textSize; // Security check
var tempByteCounter: i32 = 0; // Used to count longer text line num chars
var byteCounter: i32 = 0;
var textWidth: f32 = 0;
var tempTextWidth: f32 = 0; // Used to count longer text line width
var textHeight: f32 = fontSize;
const scaleFactor: f32 = fontSize/@as(f32, @floatFromInt(font.baseSize));
var i: usize = 0;
while (i < text.len)
{
byteCounter += 1;
var next: usize = 0;
const letter = GetCodePointNext(text[i..], &next);
const index = GetGlyphIndex(font, letter);
i += next;
if (letter != '\n')
{
if (font.glyphs[index].advanceX != 0) {
textWidth += @floatFromInt(font.glyphs[index].advanceX);
} else {
textWidth += font.recs[index].width;
textWidth += @floatFromInt(font.glyphs[index].offsetX);
}
}
else
{
if (tempTextWidth < textWidth) tempTextWidth = textWidth;
byteCounter = 0;
textWidth = 0;
textHeight += (fontSize + textLineSpacing);
}
if (tempByteCounter < byteCounter) tempByteCounter = byteCounter;
}
if (tempTextWidth < textWidth) tempTextWidth = textWidth;
textSize.x = tempTextWidth*scaleFactor + @as(f32, @floatFromInt(tempByteCounter - 1)) * spacing;
textSize.y = textHeight;
return textSize;
}
pub fn drawTextCentered(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, color: rl.Color) void {
const text_size = measureTextEx(font, text, font_size, spacing);
const adjusted_position = rl.Vector2{
.x = position.x - text_size.x/2,
.y = position.y - text_size.y/2,
pub const Stack = struct {
pub const Direction = enum {
top_to_bottom,
bottom_to_top,
left_to_right
};
drawTextEx(font, text, adjusted_position, font_size, spacing, color);
}
unused_box: rl.Rectangle,
dir: Direction,
gap: f32 = 0,
pub fn init(box: rl.Rectangle, dir: Direction) Stack {
return Stack{
.unused_box = box,
.dir = dir
};
}
pub fn next(self: *Stack, size: f32) rl.Rectangle {
return switch (self.dir) {
.top_to_bottom => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size);
self.unused_box.y += size;
self.unused_box.y += self.gap;
return next_box;
},
.bottom_to_top => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size);
self.unused_box.height -= size;
self.unused_box.height -= self.gap;
return next_box;
},
.left_to_right => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height);
self.unused_box.x += size;
self.unused_box.x += self.gap;
return next_box;
},
};
}
};
pub const IdIterator = struct {
id: Id,
counter: u32,
pub fn init(comptime src: SourceLocation) IdIterator {
return IdIterator{
.id = Id.init(src),
.counter = 0
};
}
pub fn next(self: *IdIterator) Id {
var id = self.id;
id.extra = self.counter;
self.counter += 1;
return id;
}
};

View File

@ -1,42 +0,0 @@
const rl = @import("raylib");
const Stack = @This();
pub const Direction = enum {
top_to_bottom,
bottom_to_top,
left_to_right
};
unused_box: rl.Rectangle,
dir: Direction,
gap: f32 = 0,
pub fn init(box: rl.Rectangle, dir: Direction) Stack {
return Stack{
.unused_box = box,
.dir = dir
};
}
pub fn next(self: *Stack, size: f32) rl.Rectangle {
return switch (self.dir) {
.top_to_bottom => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size);
self.unused_box.y += size;
self.unused_box.y += self.gap;
return next_box;
},
.bottom_to_top => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size);
self.unused_box.height -= size;
self.unused_box.height -= self.gap;
return next_box;
},
.left_to_right => {
const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height);
self.unused_box.x += size;
self.unused_box.x += self.gap;
return next_box;
},
};
}

View File

@ -1,183 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Position = Api.Position;
const Server = Api.Server;
const assert = std.debug.assert;
pub const Action = union(enum) {
move: Position,
fight,
gather,
deposit_gold: u64,
deposit_item: Api.ItemQuantity,
withdraw_item: Api.ItemQuantity,
craft_item: Api.ItemQuantity,
accept_task,
pub fn perform(self: Action, api: *Server, name: []const u8) !ActionResult {
const log = std.log.default;
switch (self) {
.fight => {
log.debug("[{s}] attack", .{name});
return .{
.fight = api.actionFight(name)
};
},
.move => |pos| {
log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y});
return .{
.move = api.actionMove(name, pos.x, pos.y)
};
},
.deposit_gold => |quantity| {
log.debug("[{s}] deposit {} gold", .{name, quantity});
return .{
.deposit_gold = api.actionBankDepositGold(name, quantity)
};
},
.deposit_item => |item| {
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity});
return .{
.deposit_item = api.actionBankDepositItem(name, code, item.quantity)
};
},
.withdraw_item => |item| {
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity});
return .{
.withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity)
};
},
.gather => {
log.debug("[{s}] gather", .{name});
return .{
.gather = api.actionGather(name)
};
},
.craft_item => |item| {
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity});
return .{
.craft_item = api.actionCraft(name, code, item.quantity)
};
},
.accept_task => {
log.debug("[{s}] accept task", .{name});
return .{
.accept_task = api.acceptTask(name)
};
}
}
}
};
pub const ErrorResponse = enum {
/// Something went wrong, and you probably can't reasonbly recover from it. Bail, bail!
abort,
/// You probably were trying to an action a bit too early, just try again a bit later.
retry,
/// Something in your logic went wrong, re-evaluate your state and do something different.
restart,
/// The error can be safe ignored, continue doing the next action that you wanted.
ignore
};
pub const ActionResult = union(enum) {
move: Api.MoveError!Server.MoveResult,
fight: Api.FightError!Server.FightResult,
gather: Api.GatherError!Server.GatherResult,
deposit_gold: Api.BankDepositGoldError!Server.GoldTransactionResult,
deposit_item: Api.BankDepositItemError!Server.ItemTransactionResult,
withdraw_item: Api.BankWithdrawItemError!Server.ItemTransactionResult,
craft_item: Api.CraftError!Server.CraftResult,
accept_task: Api.AcceptTaskError!Server.AcceptTaskResult,
const AnyError = Server.MoveError;
const Tag = @typeInfo(ActionResult).Union.tag_type.?;
fn fieldType(comptime kind: Tag) type {
const field_type = std.meta.fields(ActionResult)[@intFromEnum(kind)].type;
return @typeInfo(field_type).ErrorUnion.payload;
}
pub fn get(self: ActionResult, comptime kind: Tag) ?fieldType(kind) {
return switch (self) {
kind => |v| v catch null,
else => null
};
}
pub fn getError(self: ActionResult) !void {
switch (self) {
.fight => |result| {
_ = try result;
},
.move => |result| {
_ = try result;
},
.deposit_gold => |result| {
_ = try result;
},
.deposit_item => |result| {
_ = try result;
},
.withdraw_item => |result| {
_ = try result;
},
.gather => |result| {
_ = try result;
},
.craft_item => |result| {
_ = try result;
},
.accept_task => |result| {
_ = try result;
}
}
}
pub fn getErrorResponse(self: ActionResult) ?ErrorResponse {
self.getError() catch |err| switch (err) {
error.CharacterIsBusy,
error.CharacterInCooldown,
error.BankIsBusy => return ErrorResponse.retry,
error.CharacterAtDestination => return ErrorResponse.ignore,
error.MapNotFound,
error.CharacterIsFull,
error.MonsterNotFound,
error.NotEnoughSkill,
error.ResourceNotFound,
error.NotEnoughGold,
error.BankNotFound,
error.ItemNotFound,
error.NotEnoughItems,
error.RecipeNotFound,
error.AlreadyHasTask,
error.TaskMasterNotFound,
error.WorkshopNotFound => return ErrorResponse.restart,
error.CharacterNotFound,
error.ServerUnavailable,
error.RequestFailed,
error.ParseFailed,
error.OutOfMemory => return ErrorResponse.abort
};
return null;
}
};
comptime {
const ActionTag = @typeInfo(Action).Union.tag_type.?;
const ResultTag = @typeInfo(ActionResult).Union.tag_type.?;
assert(std.meta.fields(ActionTag).len == std.meta.fields(ResultTag).len);
}

457
lib/artificer.zig Normal file
View File

@ -0,0 +1,457 @@
// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
const GatherGoal = @import("gather_goal.zig");
const CraftGoal = @import("craft_goal.zig");
const EquipGoal = @import("equip_goal.zig");
const assert = std.debug.assert;
const log = std.log.scoped(.artificer);
pub const GoalId = packed struct {
const Generation = u5;
const Index = u11;
generation: Generation,
index: Index,
pub fn eql(self: GoalId, other: GoalId) bool {
return self.index == other.index and self.generation == other.generation;
}
};
const max_goals = std.math.maxInt(GoalId.Index);
const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
const server_down_retry_interval = 5; // minutes
pub const Goal = union(enum) {
gather: GatherGoal,
craft: CraftGoal,
equip: EquipGoal,
pub fn tick(self: *Goal, ctx: *GoalContext) !void {
switch (self.*) {
.gather => |*gather| gather.tick(ctx),
.craft => |*craft| try craft.tick(ctx),
.equip => |*equip| equip.tick(ctx),
}
}
pub fn requirements(self: Goal, ctx: *GoalContext) Requirements {
return switch (self) {
.gather => |gather| gather.requirements(ctx),
.craft => |craft| craft.requirements(ctx),
.equip => |equip| equip.requirements(ctx),
};
}
pub fn onActionCompleted(self: *Goal, ctx: *GoalContext, result: ActionResult) void {
switch (self.*) {
.gather => |*gather| gather.onActionCompleted(ctx, result),
.craft => |*craft| craft.onActionCompleted(ctx, result),
.equip => |*equip| equip.onActionCompleted(ctx, result),
}
}
};
const GoalSlot = struct {
generation: GoalId.Generation = 0,
parent_goal: ?GoalId = null,
goal: ?Goal = null,
};
pub const Action = union(enum) {
move: Api.Position,
gather,
craft: struct {
item: Api.Store.Id,
quantity: u64
},
unequip: struct {
slot: Api.Equipment.SlotId,
quantity: u64
},
equip: struct {
slot: Api.Equipment.SlotId,
item: Api.Store.Id,
quantity: u64
},
};
pub const ActionResult = union(enum) {
move: Api.MoveResult,
gather: Api.GatherResult,
craft: Api.CraftResult,
equip: Api.EquipResult,
unequip: Api.UnequipResult,
};
const ActionSlot = struct {
goal: GoalId,
action: Action,
};
pub const Requirements = struct {
pub const Items = Api.SimpleItem.BoundedArray(Api.Craft.max_items);
items: Items = .{}
};
const QueuedActions = std.ArrayListUnmanaged(ActionSlot);
pub const GoalContext = struct {
goal_id: GoalId,
store: *Api.Store,
character: *Api.Character,
queued_actions: *QueuedActions,
completed: bool = false,
pub fn queueAction(self: *GoalContext, action: Action) void {
self.queued_actions.appendAssumeCapacity(ActionSlot{
.goal = self.goal_id,
.action = action
});
}
pub fn findBestResourceWithItem(self: *GoalContext, item: Api.Store.Id) ?Api.Store.Id {
var best_resource: ?Api.Store.Id = null;
var best_rate: u64 = 0;
for (0.., self.store.resources.objects.items) |resource_id, optional_resource| {
if (optional_resource != .object) {
continue;
}
const resource = optional_resource.object;
const skill = resource.skill.toCharacterSkill();
const character_skill_level = self.character.skills.get(skill).level;
if (character_skill_level < resource.level) {
continue;
}
for (resource.drops.slice()) |_drop| {
const drop: Api.Resource.Drop = _drop;
if (drop.item != item) {
continue;
}
// The lower the `drop.rate` the better
if (best_resource == null or best_rate > drop.rate) {
best_resource = resource_id;
best_rate = drop.rate;
break;
}
}
}
return best_resource;
}
pub fn findNearestMapWithResource(self: *GoalContext, resource: Api.Store.Id) ?Api.Position {
const resource_code = self.store.resources.get(resource).?.code.slice();
var nearest_position: ?Api.Position = null;
for (self.store.maps.items) |map| {
const content = map.content orelse continue;
if (content.type != .resource) {
continue;
}
if (!std.mem.eql(u8, resource_code, content.code.slice())) {
continue;
}
if (nearest_position == null or map.position.distance(self.character.position) < map.position.distance(nearest_position.?)) {
nearest_position = map.position;
}
}
return nearest_position;
}
pub fn findNearestWorkstation(self: *GoalContext, skill: Api.Craft.Skill) ?Api.Position {
const skill_name = skill.toString();
var nearest_position: ?Api.Position = null;
for (self.store.maps.items) |map| {
const content = map.content orelse continue;
if (content.type != .workshop) {
continue;
}
if (!std.mem.eql(u8, skill_name, content.code.slice())) {
continue;
}
if (nearest_position == null or map.position.distance(self.character.position) < map.position.distance(nearest_position.?)) {
nearest_position = map.position;
}
}
return nearest_position;
}
};
pub fn ArtificerType(Clock: type, Server: type) type {
return struct {
const Self = @This();
clock: *Clock,
server: *Server,
store: *Api.Store,
character: Api.Store.Id,
goal_slots: []GoalSlot,
queued_actions: QueuedActions,
pub fn init(allocator: Allocator, store: *Api.Store, clock: *Clock, server: *Server, character: Api.Store.Id) !Self {
const max_queued_actions = 16;
const goal_slots = try allocator.alloc(GoalSlot, max_goals);
errdefer allocator.free(goal_slots);
@memset(goal_slots, .{});
var queued_actions = try QueuedActions.initCapacity(allocator, max_queued_actions);
errdefer queued_actions.deinit(allocator);
return Self{
.clock = clock,
.server = server,
.store = store,
.goal_slots = goal_slots,
.character = character,
.queued_actions = queued_actions
};
}
pub fn deinit(self: *Self, allocator: Allocator) void {
allocator.free(self.goal_slots);
self.queued_actions.deinit(allocator);
}
pub fn appendGoal(self: *Self, goal: Goal) !GoalId {
for (0.., self.goal_slots) |index, *goal_slot| {
if (goal_slot.goal != null) {
continue;
}
if (goal_slot.generation == std.math.maxInt(GoalId.Generation)) {
continue;
}
goal_slot.goal = goal;
return GoalId{
.index = @intCast(index),
.generation = goal_slot.generation
};
}
return error.OutOfMemory;
}
pub fn removeGoal(self: *Self, id: GoalId) void {
if (self.getGoal(id)) |goal_slot| {
goal_slot.* = .{
.generation = goal_slot.generation + 1
};
}
}
pub fn getGoal(self: *Self, id: GoalId) ?*GoalSlot {
const slot = &self.goal_slots[id.index];
if (slot.generation != id.generation) {
return null;
}
if (slot.goal == null) {
return null;
}
return slot;
}
fn timeUntilCooldownExpires(self: *Self) u64 {
const store = self.server.store;
const character = store.characters.get(self.character).?;
if (character.cooldown_expiration) |cooldown_expiration| {
const cooldown_expiration_ns: i64 = @intFromFloat(cooldown_expiration * std.time.ns_per_s);
const now = self.clock.nanoTimestamp();
if (cooldown_expiration_ns > now) {
return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now);
}
}
return 0;
}
fn getGoalCount(self: *Self) u32 {
var count: u32 = 0;
for (self.goal_slots) |goal_slot| {
if (goal_slot.goal == null) {
continue;
}
count += 1;
}
return count;
}
fn hasSubGoals(self: *Self, goal_id: GoalId) bool {
for (self.goal_slots) |goal_slot| {
if (goal_slot.goal != null and goal_slot.parent_goal != null and goal_slot.parent_goal.?.eql(goal_id)) {
return true;
}
}
return false;
}
fn createGoalContext(self: *Self, goal_id: GoalId) GoalContext {
return GoalContext{
.goal_id = goal_id,
.queued_actions = &self.queued_actions,
.character = self.store.characters.get(self.character).?,
.store = self.store,
.completed = false,
};
}
pub fn tick(self: *Self) !void {
const store = self.server.store;
const character = store.characters.get(self.character).?;
if (self.queued_actions.items.len > 0) {
const expires_in = self.timeUntilCooldownExpires();
if (expires_in > 0) {
return;
}
const action_slot = self.queued_actions.orderedRemove(0);
const character_name = character.name.slice();
log.debug("(action) {}", .{ action_slot.action });
const action_result = switch (action_slot.action) {
.move => |position| ActionResult{
.move = try self.server.move(character_name, position)
},
.gather => ActionResult{
.gather = try self.server.gather(character_name)
},
.craft => |craft| ActionResult{
.craft = try self.server.craft(character_name, store.items.getName(craft.item).?, craft.quantity)
},
.equip => |equip| ActionResult{
.equip = try self.server.equip(character_name, equip.slot, store.items.getName(equip.item).?, equip.quantity)
},
.unequip => |unequip| ActionResult{
.unequip = try self.server.unequip(character_name, unequip.slot, unequip.quantity)
}
};
if (self.getGoal(action_slot.goal)) |goal_slot| {
const goal = &goal_slot.goal.?;
var goal_context = self.createGoalContext(action_slot.goal);
goal.onActionCompleted(&goal_context, action_result);
if (goal_context.completed) {
self.removeGoal(action_slot.goal);
}
}
} else {
for (0.., self.goal_slots) |index, *goal_slot| {
if (goal_slot.goal == null) {
continue;
}
const goal = &(goal_slot.*.goal orelse continue);
const goal_id = GoalId{
.index = @intCast(index),
.generation = goal_slot.generation
};
if (self.hasSubGoals(goal_id)) {
continue;
}
var goal_context = self.createGoalContext(goal_id);
const reqs = goal.requirements(&goal_context);
for (reqs.items.slice()) |req_item| {
const inventory_quantity = character.inventory.getQuantity(req_item.id);
if (inventory_quantity < req_item.quantity) {
const missing_quantity = req_item.quantity - inventory_quantity;
const item = store.items.get(req_item.id).?;
if (goal_context.findBestResourceWithItem(req_item.id) != null) {
const subgoal_id = try self.appendGoal(.{
.gather = .{
.item = req_item.id,
.quantity = missing_quantity
}
});
const subgoal = self.getGoal(subgoal_id).?;
subgoal.parent_goal = goal_id;
} else if (item.craft != null) {
const subgoal_id = try self.appendGoal(.{
.craft = .{
.item = req_item.id,
.quantity = missing_quantity
}
});
const subgoal = self.getGoal(subgoal_id).?;
subgoal.parent_goal = goal_id;
} else {
@panic("Not all requirements were handled");
}
}
}
if (self.hasSubGoals(goal_id)) {
continue;
}
try goal.tick(&goal_context);
if (goal_context.completed) {
self.removeGoal(goal_id);
}
}
}
}
pub fn runUntilGoalsComplete(self: *Self) !void {
while (self.getGoalCount() > 0) {
const expires_in = self.timeUntilCooldownExpires();
if (expires_in > 0) {
log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
self.clock.sleep(expires_in);
}
try self.tick();
}
}
pub fn runForever(self: *Self) !void {
while (true) {
const expires_in = self.timeUntilCooldownExpires();
if (expires_in > 0) {
log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
self.clock.sleep(expires_in);
log.debug("Finished sleeping", .{});
}
try self.tick();
}
}
};
}

View File

@ -1,104 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Server = Api.Server;
const Position = Api.Position;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const CharacterTask = @import("./task.zig").Task;
const QueuedAction = @import("./action.zig").Action;
const QueuedActionResult = @import("./action.zig").ActionResult;
const Brain = @This();
name: []const u8,
action_queue: std.ArrayList(QueuedAction),
task: ?CharacterTask = null,
paused_until: ?i64 = null, // ms
pub fn init(allocator: Allocator, name: []const u8) !Brain {
return Brain{
.name = try allocator.dupe(u8, name),
.action_queue = std.ArrayList(QueuedAction).init(allocator),
};
}
pub fn deinit(self: Brain) void {
const allocator = self.action_queue.allocator;
allocator.free(self.name);
self.action_queue.deinit();
}
pub fn performNextAction(self: *Brain, api: *Server) !void {
const log = std.log.default;
assert(self.action_queue.items.len > 0);
const retry_delay = 500; // 500ms
const next_action = self.action_queue.items[0];
const action_result = try next_action.perform(api, self.name);
if (action_result.getErrorResponse()) |error_response| {
switch (error_response) {
.retry => {
self.paused_until = std.time.milliTimestamp() + retry_delay;
log.warn("[{s}] retry action", .{self.name});
return;
},
.restart => {
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
.abort => {
log.warn("[{s}] abort action {s}", .{ self.name, @tagName(next_action) });
try action_result.getError();
// The error above should always return
unreachable;
},
.ignore => { },
}
}
_ = self.action_queue.orderedRemove(0);
if (self.task) |*task| {
task.onActionCompleted(action_result);
}
}
pub fn step(self: *Brain, api: *Api.Server) !void {
if (self.paused_until) |paused_until| {
if (std.time.milliTimestamp() < paused_until) {
return;
}
self.paused_until = null;
}
if (self.action_queue.items.len > 0) return;
if (self.task) |task| {
if (task.isComplete()) {
// if (try brain.depositItemsToBank(&self.server)) {
// continue;
// }
self.task = null;
}
}
if (self.task) |task| {
try task.queueActions(api, self.name, &self.action_queue);
}
}
pub fn cooldown(self: *Brain, api: *Server) i64 {
const character = api.store.getCharacter(self.name).?;
const cooldown_expiration: i64 = @intFromFloat(character.cooldown_expiration * std.time.ms_per_s);
if (self.paused_until) |pause_until| {
return @max(cooldown_expiration, pause_until);
} else {
return cooldown_expiration;
}
}

77
lib/craft_goal.zig Normal file
View File

@ -0,0 +1,77 @@
// zig fmt: off
const Api = @import("artifacts-api");
const Artificer = @import("./artificer.zig");
const Context = Artificer.GoalContext;
const Requirements = Artificer.Requirements;
const Goal = @This();
item: Api.Store.Id,
quantity: u64,
fn getCraftMultiples(self: Goal, craft: Api.Craft) u64 {
return @intFromFloat(@ceil(
@as(f32, @floatFromInt(self.quantity)) /
@as(f32, @floatFromInt(craft.quantity))
));
}
pub fn tick(self: *Goal, ctx: *Context) !void {
const store = ctx.store;
if (self.quantity == 0) {
ctx.completed = true;
return;
}
const item = store.items.get(self.item).?;
const craft = item.craft.?;
const skill = craft.skill.toCharacterSkill();
if (ctx.character.skills.get(skill).level < craft.level) {
return error.SkillTooLow;
}
const workshop_position = ctx.findNearestWorkstation(craft.skill).?;
if (!workshop_position.eql(ctx.character.position)) {
ctx.queueAction(.{
.move = workshop_position
});
return;
}
ctx.queueAction(.{
.craft = .{ .item = self.item, .quantity = self.quantity }
});
}
pub fn requirements(self: Goal, ctx: *Context) Requirements {
var reqs: Requirements = .{};
const store = ctx.store;
const item = store.items.get(self.item).?;
const craft = item.craft.?;
const craft_multiples = self.getCraftMultiples(craft);
for (craft.items.slice()) |craft_item| {
reqs.items.addAssumeCapacity(craft_item.id, craft_item.quantity * craft_multiples);
}
return reqs;
}
pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void {
_ = ctx;
if (result == .craft) {
const craft_result = result.craft;
const craft_quantity = craft_result.details.items.getQuantity(self.item);
if (self.quantity > craft_quantity) {
self.quantity -= craft_quantity;
} else {
self.quantity = 0;
}
}
}

56
lib/equip_goal.zig Normal file
View File

@ -0,0 +1,56 @@
// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const Artificer = @import("./artificer.zig");
const Context = Artificer.GoalContext;
const Requirements = Artificer.Requirements;
const Goal = @This();
slot: Api.Character.Equipment.SlotId,
item: Api.Store.Id,
quantity: u64 = 1,
pub fn tick(self: *Goal, ctx: *Context) void {
const character = ctx.character;
const equipment_slot = character.equipment.slots.get(self.slot);
if (equipment_slot.item) |equiped_item|{
if (equiped_item == self.item and !self.slot.canHoldManyItems()) {
ctx.completed = true;
} else {
ctx.queueAction(.{
.unequip = .{
.slot = self.slot,
.quantity = self.quantity
}
});
}
return;
}
ctx.queueAction(.{
.equip = .{
.slot = self.slot,
.item = self.item,
.quantity = self.quantity
}
});
}
pub fn requirements(self: Goal, ctx: *Context) Requirements {
_ = ctx;
var reqs: Artificer.Requirements = .{};
reqs.items.addAssumeCapacity(self.item, self.quantity);
return reqs;
}
pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void {
_ = self;
if (result == .equip) {
ctx.completed = true;
}
}

59
lib/gather_goal.zig Normal file
View File

@ -0,0 +1,59 @@
// zig fmt: off
const Api = @import("artifacts-api");
const Artificer = @import("./artificer.zig");
const Context = Artificer.GoalContext;
const Requirements = Artificer.Requirements;
const Goal = @This();
item: Api.Store.Id,
quantity: u64,
pub fn tick(self: *Goal, ctx: *Context) void {
if (self.quantity == 0) {
ctx.completed = true;
return;
}
const resource_id = ctx.findBestResourceWithItem(self.item) orelse @panic("Failed to find resource with item");
const map_position: Api.Position = ctx.findNearestMapWithResource(resource_id) orelse @panic("Map with resource not found");
if (!map_position.eql(ctx.character.position)) {
ctx.queueAction(.{
.move = map_position
});
return;
}
// TODO: Check for enough space in invetory? Or add it as a requirement
ctx.queueAction(.{
.gather = {}
});
}
pub fn requirements(self: Goal, ctx: *Context) Requirements {
_ = ctx;
_ = self;
const reqs: Requirements = .{};
// TODO: add skill requirement
return reqs;
}
pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void {
_ = ctx;
if (result == .gather) {
const gather_result = result.gather;
const gather_quantity = gather_result.details.items.getQuantity(self.item);
if (self.quantity > gather_quantity) {
self.quantity -= gather_quantity;
} else {
self.quantity = 0;
}
}
}

View File

@ -1,136 +1,11 @@
const std = @import("std");
// zig fmt: off
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
pub const Brain = @import("./brain.zig");
pub const TaskGraph = @import("./task_graph.zig");
pub const ArtificerType = @import("./artificer.zig").ArtificerType;
pub const SimServer = @import("./sim_server.zig");
pub const SimClock = @import("./sim_clock.zig");
pub const SystemClock = @import("./system_clock.zig");
const Artificer = @This();
pub const ArtificerApi = ArtificerType(SystemClock, Api.Server);
pub const ArtificerSim = ArtificerType(SimClock, SimServer);
const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
const server_down_retry_interval = 5; // minutes
server: Api.Server,
characters: std.ArrayList(Brain),
task_graph: TaskGraph,
paused_until: ?i64 = null, // ms
pub fn init(allocator: Allocator, token: []const u8) !Artificer {
var server = try Api.Server.init(allocator);
errdefer server.deinit();
try server.setToken(token);
var characters = std.ArrayList(Brain).init(allocator);
errdefer characters.deinit(); // TODO: Add character deinit
const chars = try server.listMyCharacters();
defer chars.deinit();
for (chars.items) |char| {
try characters.append(try Brain.init(allocator, char.name));
}
return Artificer{
.server = server,
.characters = characters,
.task_graph = TaskGraph.init(allocator)
};
}
pub fn deinit(self: *Artificer) void {
for (self.characters.items) |brain| {
brain.deinit();
}
self.characters.deinit();
self.server.deinit();
}
pub fn step(self: *Artificer) !void {
if (self.paused_until) |paused_until| {
if (std.time.milliTimestamp() < paused_until) {
return;
}
self.paused_until = null;
}
runNextActions(self.characters.items, &self.server) catch |err| switch (err) {
Api.FetchError.ServerUnavailable => {
self.paused_until = std.time.milliTimestamp() + std.time.ms_per_min * server_down_retry_interval;
std.log.warn("Server is down, retrying in {}min", .{ server_down_retry_interval });
return;
},
else => return err
};
for (self.characters.items) |*brain| {
if (brain.task != null) {
try brain.step(&self.server);
continue;
}
const character = self.server.store.getCharacter(brain.name).?;
if (character.task) |taskmaster_task| {
if (taskmaster_task.total > taskmaster_task.progress) {
switch (taskmaster_task.type) {
.monsters => {
const monster_code = self.server.store.getCode(taskmaster_task.target_id).?;
const maps = try self.server.getMaps(.{ .code = monster_code });
defer maps.deinit();
if (maps.items.len > 0) {
const resource_map: Api.Map = maps.items[0];
std.debug.print("fight at {}\n", .{resource_map.position});
brain.task = .{
.fight = .{
.at = resource_map.position,
.until = .{ .quantity = taskmaster_task.total - taskmaster_task.progress },
}
};
}
},
.crafts => {},
.resources => {},
}
}
} else {
brain.task = .{ .accept_task = .{} };
}
}
}
pub fn nextStepAt(self: *Artificer) i64 {
if (self.paused_until) |paused_until| {
return paused_until;
}
return earliestCooldown(self.characters.items, &self.server) orelse 0;
}
fn earliestCooldown(characters: []Brain, api: *Api.Server) ?i64 {
var earliest_cooldown: ?i64 = null;
for (characters) |*brain| {
if (brain.action_queue.items.len == 0) continue;
const cooldown = brain.cooldown(api);
if (earliest_cooldown == null or earliest_cooldown.? > cooldown) {
earliest_cooldown = cooldown;
}
}
return earliest_cooldown;
}
fn runNextActions(characters: []Brain, api: *Api.Server) !void {
for (characters) |*brain| {
if (brain.action_queue.items.len == 0) continue;
const cooldown = brain.cooldown(api);
if (std.time.milliTimestamp() >= cooldown) {
try brain.performNextAction(api);
}
}
}

13
lib/sim_clock.zig Normal file
View File

@ -0,0 +1,13 @@
// zig fmt: off
const std = @import("std");
const Clock = @This();
timestamp: i128 = 0,
pub fn sleep(self: *Clock, nanoseconds: u64) void {
self.timestamp += @intCast(nanoseconds);
}
pub fn nanoTimestamp(self: Clock) i128 {
return self.timestamp;
}

227
lib/sim_server.zig Normal file
View File

@ -0,0 +1,227 @@
// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const SimClock = @import("./sim_clock.zig");
const Server = @This();
const max_level = 40;
// https://docs.artifactsmmo.com/concepts/skills#experience-to-level
const max_xp_per_level = [max_level]u64{
150, // level 1
250,
350,
450,
700,
950,
1200,
1450,
1700,
2100, // level 10
2500,
2900,
3300,
3700,
4400,
5100,
5800,
6500,
7200,
8200, // level 20
9200,
10200,
11200,
12200,
13400,
14600,
15800,
17000,
18200,
19700, // level 30
21200,
22700,
24200,
25700,
27200,
28700,
30500,
32300,
34100,
35900, // level 40
};
store: *Api.Store,
clock: SimClock = .{},
rng: std.Random.DefaultPrng,
pub fn init(seed: u64, store: *Api.Store) Server {
return Server{
.rng = std.Random.DefaultPrng.init(seed),
.store = store
};
}
fn sleepNorm(self: *Server, stddev_ns: u64, mean_ns: u64) void {
const stddev_ns_f64: f64 = @floatFromInt(stddev_ns);
const mean_ns_f64: f64 = @floatFromInt(mean_ns);
var rng = self.rng.random();
const duration = rng.floatNorm(f64) * stddev_ns_f64 + mean_ns_f64;
self.clock.sleep(@intFromFloat(duration));
}
fn sleepRequestBegin(self: *Server) void {
self.sleepNorm(
100 * std.time.ns_per_ms,
350 * std.time.ns_per_ms,
);
}
fn sleepRequestEnd(self: *Server) void {
self.sleepNorm(
10 * std.time.ns_per_ms,
300 * std.time.ns_per_ms,
);
}
pub fn move(self: *Server, character_name: []const u8, position: Api.Position) Api.MoveError!Api.MoveResult {
self.sleepRequestBegin();
defer self.sleepRequestEnd();
const map: *Api.Map = self.store.getMap(position) orelse return Api.MoveError.MapNotFound;
const character_id = self.store.characters.getId(character_name) orelse return Api.MoveError.CharacterNotFound;
const character: *Api.Character = self.store.characters.get(character_id).?;
if (character.position.eql(position)) {
return Api.MoveError.CharacterAlreadyMap;
}
const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s;
if (character.cooldown_expiration) |cooldown_expiration| {
if (cooldown_expiration > now) {
return Api.MoveError.CharacterInCooldown;
}
}
const duration_i64 = (@abs(position.x - character.position.x) + @abs(position.y - character.position.y)) * 5;
const duration_f64: f64 = @floatFromInt(duration_i64);
const expiration = now + duration_f64;
character.cooldown_expiration = expiration;
character.position = position;
return Api.MoveResult{
.cooldown = Api.Cooldown{
.reason = .movement,
.started_at = now,
.expiration = expiration
},
.character = character.*,
.destination = map.*
};
}
pub fn gather(self: *Server, character_name: []const u8) Api.GatherError!Api.GatherResult {
self.sleepRequestBegin();
defer self.sleepRequestEnd();
const character_id = self.store.characters.getId(character_name) orelse return Api.GatherError.CharacterNotFound;
const character: *Api.Character = self.store.characters.get(character_id).?;
const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s;
if (character.cooldown_expiration) |cooldown_expiration| {
if (cooldown_expiration > now) {
return Api.GatherError.CharacterInCooldown;
}
}
const map: *Api.Map = self.store.getMap(character.position) orelse return Api.GatherError.FatalError;
const map_content = map.content orelse return Api.GatherError.MapContentNotFound;
if (map_content.type != .resource) {
return Api.GatherError.MapContentNotFound;
}
const map_content_code = map_content.code.slice();
const resource_id = self.store.resources.getId(map_content_code) orelse return Api.GatherError.MapContentNotFound;
const resource = self.store.resources.get(resource_id) orelse return Api.GatherError.FatalError;
const character_skill = character.skills.getPtr(resource.skill.toCharacterSkill());
if (character_skill.level < resource.level) {
return Api.GatherError.CharacterNotSkillLevelRequired;
}
const duration = 25; // TODO: Update duration calculation
const expiration = now + duration;
var items: Api.GatherResult.Details.Items = .{};
const xp = 8; // TODO: Update xp calculation
var rng = self.rng.random();
for (resource.drops.slice()) |_drop| {
const drop: Api.Resource.Drop = _drop;
if (rng.uintLessThan(u64, drop.rate) == 0) {
const quantity = rng.intRangeAtMost(u64, drop.min_quantity, drop.max_quantity);
items.add(drop.item, quantity) catch return Api.GatherError.FatalError;
}
}
var inventory = character.inventory;
inventory.addSlice(items.slice()) catch return Api.GatherError.CharacterInventoryFull;
if (inventory.totalQuantity() > character.inventory_max_items) {
return Api.GatherError.CharacterInventoryFull;
}
character.inventory = inventory;
character_skill.xp += xp;
while (character_skill.xp > character_skill.max_xp and character_skill.level < max_level) {
character_skill.level += 1;
character_skill.max_xp = max_xp_per_level[character_skill.level - 1];
}
if (character_skill.level == max_level) {
character_skill.xp = 0;
character_skill.max_xp = 0;
}
character.cooldown_expiration = expiration;
return Api.GatherResult{
.character = character.*,
.cooldown = Api.Cooldown{
.reason = .gathering,
.started_at = now,
.expiration = expiration
},
.details = Api.GatherResult.Details{
.xp = xp,
.items = items
}
};
}
pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) Api.CraftError!Api.CraftResult {
_ = self;
_ = character;
_ = item;
_ = quantity;
return Api.FetchError.FatalError;
}
pub fn equip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, item: []const u8, quantity: u64) Api.EquipError!Api.EquipResult {
_ = self;
_ = character;
_ = item;
_ = slot;
_ = quantity;
return Api.FetchError.FatalError;
}
pub fn unequip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, quantity: u64) Api.EquipError!Api.EquipResult {
_ = self;
_ = character;
_ = slot;
_ = quantity;
return Api.FetchError.FatalError;
}

12
lib/system_clock.zig Normal file
View File

@ -0,0 +1,12 @@
const std = @import("std");
const Clock = @This();
pub fn sleep(self: *Clock, nanoseconds: u64) void {
_ = self;
std.time.sleep(nanoseconds);
}
pub fn nanoTimestamp(self: *Clock) i128 {
_ = self;
return std.time.nanoTimestamp();
}

View File

@ -1,302 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Position = Api.Position;
const Action = @import("./action.zig").Action;
const ActionResult = @import("./action.zig").ActionResult;
const CodeId = Api.CodeId;
const ItemQuantity = Api.ItemQuantity;
const bank_position = Position{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
const task_master_position = Position{ .x = 1, .y = 2 }; // TODO: Figure this out dynamically
pub const UntilCondition = union(enum) {
xp: u64,
item: Api.ItemQuantity,
quantity: u64,
fn isComplete(self: UntilCondition, progress: u64) bool {
return switch (self) {
.xp => |xp| progress >= xp,
.item => |item| progress >= item.quantity,
.quantity => |quantity| progress >= quantity,
};
}
};
pub const Task = union(enum) {
fight: struct {
at: Position,
until: UntilCondition,
progress: u64 = 0,
},
gather: struct {
at: Position,
until: UntilCondition,
progress: u64 = 0,
},
craft: struct {
at: Position,
target: Api.ItemQuantity,
progress: u64 = 0,
},
accept_task: struct {
done: bool = false
},
pub fn isComplete(self: Task) bool {
return switch (self) {
.fight => |args| args.until.isComplete(args.progress),
.gather => |args| args.until.isComplete(args.progress),
.craft => |args| args.progress >= args.target.quantity,
.accept_task => |args| args.done
};
}
pub fn onActionCompleted(self: *Task, result: ActionResult) void {
switch (self.*) {
.fight => |*args| {
if (result.get(.fight)) |r| {
const fight_result: Api.Server.FightResult = r;
switch (args.until) {
.xp => {
args.progress += fight_result.fight.xp;
},
.item => {
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.until.item.id);
},
.quantity => {
args.progress += 1;
}
}
}
},
.gather => |*args| {
if (result.get(.gather)) |r| {
const gather_result: Api.Server.GatherResult = r;
switch (args.until) {
.xp => {
args.progress += gather_result.details.xp;
},
.item => {
const items = gather_result.details.items;
args.progress += items.getQuantity(args.until.item.id);
},
.quantity => {
args.progress += 1;
}
}
}
},
.craft => |*args| {
if (result.get(.craft_item)) |r| {
const craft_result: Api.Server.CraftResult = r;
const items = craft_result.details.items;
args.progress += items.getQuantity(args.target.id);
}
},
.accept_task => |*args| {
if (result.get(.accept_task)) |_| {
args.done = true;
}
}
}
}
pub fn queueActions(self: Task, api: *Api.Server, name: []const u8, action_queue: *std.ArrayList(Action)) !void {
const ctx = TaskContext{
.api = api,
.name = name,
.action_queue = action_queue
};
switch (self) {
.fight => |args| {
try ctx.fightRoutine(args.at);
},
.gather => |args| {
try ctx.gatherRoutine(args.at);
},
.craft => |args| {
try ctx.craftRoutine(args.at, args.target.id, args.target.quantity);
},
.accept_task => {
if (try ctx.moveIfNeeded(task_master_position)) {
return;
}
try ctx.action_queue.append(.{ .accept_task = {} });
}
}
}
};
const TaskContext = struct {
api: *Api.Server,
name: []const u8,
action_queue: *std.ArrayList(Action),
fn getCharacter(self: TaskContext) Api.Character {
return self.api.store.getCharacter(self.name).?;
}
fn moveIfNeeded(self: TaskContext, pos: Position) !bool {
const character = self.getCharacter();
if (character.position.eql(pos)) {
return false;
}
try self.action_queue.append(.{ .move = pos });
return true;
}
pub fn depositItemsToBank(self: TaskContext) !bool {
var character = self.getCharacter();
const action_queue = self.action_queue;
// Deposit items and gold to bank if full
if (character.getItemCount() == 0) {
return false;
}
if (!character.position.eql(bank_position)) {
try action_queue.append(.{ .move = bank_position });
}
for (character.inventory.slice()) |slot| {
try action_queue.append(.{
.deposit_item = .{ .id = slot.id, .quantity = slot.quantity }
});
}
return true;
}
fn depositIfFull(self: TaskContext) !bool {
const character = self.getCharacter();
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
_ = try depositItemsToBank(self);
if (character.gold > 0) {
try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
}
return true;
}
fn fightRoutine(self: TaskContext, enemy_position: Position) !void {
if (try self.depositIfFull()) {
return;
}
if (try self.moveIfNeeded(enemy_position)) {
return;
}
try self.action_queue.append(.{ .fight = {} });
}
fn gatherRoutine(self: TaskContext, resource_position: Position) !void {
if (try self.depositIfFull()) {
return;
}
if (try self.moveIfNeeded(resource_position)) {
return;
}
try self.action_queue.append(.{ .gather = {} });
}
fn withdrawFromBank(self: TaskContext, items: []const ItemQuantity) !bool {
const character = self.getCharacter();
var has_all_items = true;
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
}
}
if (has_all_items) return false;
if (try self.moveIfNeeded(bank_position)) {
return true;
}
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try self.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity,
}});
}
}
return true;
}
fn craftItem(self: TaskContext, workstation: Position, id: CodeId, quantity: u64) !bool {
var character = self.getCharacter();
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
return false;
}
if (try self.moveIfNeeded(workstation)) {
return true;
}
try self.action_queue.append(.{ .craft_item = .{
.id = id,
.quantity = quantity - inventory_quantity
}});
return true;
}
fn craftRoutine(self: TaskContext, workstation: Position, id: CodeId, quantity: u64) !void {
var character = self.getCharacter();
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
if (try self.depositItemsToBank()) {
return;
}
}
const code = self.api.store.getCode(id) orelse return error.InvalidItemId;
const target_item = try self.api.getItem(code) orelse return error.ItemNotFound;
if (target_item.craft == null) {
return error.NotCraftable;
}
const recipe = target_item.craft.?;
var needed_items = recipe.items;
for (needed_items.slice()) |*needed_item| {
needed_item.quantity *= quantity;
}
if (try self.withdrawFromBank(needed_items.slice())) {
return;
}
if (try self.craftItem(workstation, id, quantity)) {
return;
}
}
};

View File

@ -1,197 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
const TaskGraph = @This();
const CharacterTask = @import("./task.zig").Task;
const TaskNodeId = u16;
const TaskNode = struct {
const Dependencies = std.BoundedArray(TaskNodeId, 8);
const MissingItems = Api.BoundedSlotsArray(8);
task: CharacterTask,
dependencies: Dependencies = Dependencies.init(0) catch unreachable,
missing_items: MissingItems = MissingItems.init(),
};
const Nodes = std.ArrayList(TaskNode);
nodes: Nodes,
pub fn init(allocator: Allocator) TaskGraph {
return TaskGraph{ .nodes = Nodes.init(allocator) };
}
pub fn deinit(self: TaskGraph) void {
self.nodes.deinit();
}
fn get(self: *TaskGraph, id: TaskNodeId) *TaskNode {
return &self.nodes.items[id];
}
fn addTask(self: *TaskGraph, node: TaskNode) !TaskNodeId {
try self.nodes.append(node);
return @intCast(self.nodes.items.len-1);
}
fn addFightTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
const monsters = try api.getMonsters(.{ .drop = item_code });
defer monsters.deinit();
if (monsters.items.len == 0) return error.ResourceNotFound;
if (monsters.items.len > 1) std.log.warn("Multiple monsters exist for target item", .{});
const monster_code = monsters.items[0].code;
const resource_maps = try self.api.getMaps(.{ .code = monster_code });
defer resource_maps.deinit();
// This monster currently doesn't exist on the map. Probably only spawns in certain situations.
if (resource_maps.items.len == 0) return error.MapNotFound;
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{});
const resource_map = resource_maps.items[0];
return try self.addTask(TaskNode{
.task = .{
.fight = .{
.at = resource_map.position,
.until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
}
}
});
}
fn addGatherTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
const resources = try api.getResources(.{ .drop = item_code });
defer resources.deinit();
if (resources.items.len == 0) return error.ResourceNotFound;
if (resources.items.len > 1) std.log.warn("Multiple resources exist for target item", .{});
const resource_code = resources.items[0].code;
const resource_maps = try self.api.getMaps(.{ .code = resource_code });
defer resource_maps.deinit();
if (resource_maps.items.len == 0) return error.MapNotFound;
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for resource", .{});
const resource_map = resource_maps.items[0];
return try self.addTask(TaskNode{
.task = .{
.gather = .{
.at = resource_map.position,
.until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
}
}
});
}
fn addCraftTaskShallow(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
const recipe = item.craft orelse return error.RecipeNotFound;
const skill_str = Api.Server.SkillUtils.toString(recipe.skill);
const workshop_maps = try self.api.getMaps(.{ .code = skill_str, .type = .workshop });
defer workshop_maps.deinit();
if (workshop_maps.items.len == 0) return error.WorkshopNotFound;
if (workshop_maps.items.len > 1) std.log.warn("Multiple workshop locations exist", .{});
return try self.addTask(TaskNode{
.task = .{
.craft = .{
.at = workshop_maps.items[0].position,
.target = Api.ItemQuantity.init(item_id, quantity)
}
}
});
}
fn addCraftTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const node_id = try self.addCraftTaskShallow(api, item_id, quantity);
var node = self.get(node_id);
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
const recipe = item.craft orelse return error.RecipeNotFound;
const craft_count = recipe.quantity;
for (recipe.items.slots.constSlice()) |material| {
const needed_quantity = material.quantity * craft_count;
if (try self.addAutoTask(api, material.id, needed_quantity)) |dependency_id| {
try node.dependencies.append(dependency_id);
} else {
try node.missing_items.add(material.id, needed_quantity);
}
}
return node_id;
}
// TODO: Remove `anyerror` from function declaration
fn addAutoTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) anyerror!?TaskNodeId {
const item = (try self.api.getItemById(item_id)) orelse return error.ItemNotFound;
if (item.craft != null) {
return try self.addCraftTask(api, item_id, quantity);
} else if (item.type == .resource) {
const eql = std.mem.eql;
if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) {
return try self.addGatherTask(api, item_id, quantity);
} else if (eql(u8, item.subtype, "mob")) {
return try self.addFightTask(api, item_id, quantity);
}
}
return null;
}
fn printTask(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId) void {
self.printTaskLevel(api, node_id, 0);
}
fn writeIdentation(level: u32) void {
const mutex = std.debug.getStderrMutex();
mutex.lock();
defer mutex.unlock();
const stderr = std.io.getStdErr().writer();
stderr.writeBytesNTimes(" ", level) catch return;
}
fn printTaskLevel(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId, level: u32) void {
const node = self.get(node_id);
const print = std.debug.print;
writeIdentation(level);
switch (node.task) {
.fight => |args| {
const target_item = args.until.item;
const item = api.store.getCode(target_item.id).?;
print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
},
.gather => |args| {
const target_item = args.until.item;
const item = api.store.getCode(target_item.id).?;
print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
},
.craft => |args| {
const item = api.store.getCode(args.target.id).?;
print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at});
},
}
for (node.dependencies.constSlice()) |dependency| {
self.printTaskLevel(dependency, level + 1);
}
for (node.missing_items.slots.constSlice()) |slot| {
const item_code = api.getItemCode(slot.id).?;
writeIdentation(level+1);
print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
}
}