Compare commits
9 Commits
5188d778be
...
b6859909ab
Author | SHA1 | Date | |
---|---|---|---|
b6859909ab | |||
4e0f33e2ff | |||
cdd11147d3 | |||
87c09f1e27 | |||
df56eceab6 | |||
d6a141d098 | |||
522840ca22 | |||
b80a356739 | |||
bbdf0511b9 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.zig-cache
|
||||
zig-out
|
||||
result
|
||||
token
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
217
api/errors.zig
217
api/errors.zig
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
83
api/ratelimit.zig
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
37
api/root.zig
37
api/root.zig
@ -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;
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
@ -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)
|
||||
};
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
@ -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)
|
||||
};
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
@ -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
|
||||
};
|
||||
}
|
37
api/schemas/equip_result.zig
Normal file
37
api/schemas/equip_result.zig
Normal 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;
|
||||
}
|
@ -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"),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
@ -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
43
api/schemas/ge_order.zig
Normal 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));
|
||||
}
|
@ -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("}");
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
@ -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).?.*;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
31
api/schemas/move_result.zig
Normal file
31
api/schemas/move_result.zig
Normal 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
39
api/schemas/position.zig
Normal 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 });
|
||||
}
|
@ -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
152
api/schemas/simple_item.zig
Normal 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("}");
|
||||
}
|
||||
};
|
||||
}
|
@ -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),
|
||||
};
|
||||
}
|
@ -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 },
|
||||
});
|
@ -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)
|
||||
};
|
||||
}
|
@ -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),
|
||||
};
|
||||
}
|
23
api/schemas/skill_info_details.zig
Normal file
23
api/schemas/skill_info_details.zig
Normal 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)
|
||||
};
|
||||
}
|
@ -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),
|
||||
};
|
||||
}
|
33
api/schemas/skill_usage_result.zig
Normal file
33
api/schemas/skill_usage_result.zig
Normal 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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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("}");
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
1533
api/server.zig
1533
api/server.zig
File diff suppressed because it is too large
Load Diff
28
api/stb_image/root.zig
Normal file
28
api/stb_image/root.zig
Normal 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
23
api/stb_image/stb_image.c
Normal 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
7988
api/stb_image/stb_image.h
Normal file
File diff suppressed because it is too large
Load Diff
920
api/store.zig
920
api/store.zig
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
71
cli/main.zig
71
cli/main.zig
@ -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
698
gui/app.zig
Normal 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
24
gui/base.vsh
Normal 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
45
gui/blur.fsh
Normal 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
138
gui/font-face.zig
Normal 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);
|
||||
}
|
148
gui/main.zig
148
gui/main.zig
@ -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
202
gui/roboto-font/LICENSE.txt
Normal 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.
|
BIN
gui/roboto-font/Roboto-Black.ttf
Normal file
BIN
gui/roboto-font/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-BlackItalic.ttf
Normal file
BIN
gui/roboto-font/Roboto-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-Bold.ttf
Normal file
BIN
gui/roboto-font/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-BoldItalic.ttf
Normal file
BIN
gui/roboto-font/Roboto-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-Italic.ttf
Normal file
BIN
gui/roboto-font/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-Light.ttf
Normal file
BIN
gui/roboto-font/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-LightItalic.ttf
Normal file
BIN
gui/roboto-font/Roboto-LightItalic.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-Medium.ttf
Normal file
BIN
gui/roboto-font/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-MediumItalic.ttf
Normal file
BIN
gui/roboto-font/Roboto-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-Regular.ttf
Normal file
BIN
gui/roboto-font/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-Thin.ttf
Normal file
BIN
gui/roboto-font/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
gui/roboto-font/Roboto-ThinItalic.ttf
Normal file
BIN
gui/roboto-font/Roboto-ThinItalic.ttf
Normal file
Binary file not shown.
319
gui/ui.zig
319
gui/ui.zig
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
183
lib/action.zig
183
lib/action.zig
@ -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
457
lib/artificer.zig
Normal 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
104
lib/brain.zig
104
lib/brain.zig
@ -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
77
lib/craft_goal.zig
Normal 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
56
lib/equip_goal.zig
Normal 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
59
lib/gather_goal.zig
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
139
lib/root.zig
139
lib/root.zig
@ -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
13
lib/sim_clock.zig
Normal 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
227
lib/sim_server.zig
Normal 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
12
lib/system_clock.zig
Normal 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();
|
||||
}
|
302
lib/task.zig
302
lib/task.zig
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user