Compare commits
No commits in common. "b6859909ab025e78f387fc7b7539d3efb74288b9" and "5188d778befd9ca21a190cd85d9f8fcdd8242d2e" have entirely different histories.
b6859909ab
...
5188d778be
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
.zig-cache
|
||||
zig-out
|
||||
result
|
||||
token
|
||||
|
@ -1,15 +1,15 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
// TODO: use `EnumFieldStruct` for `str_to_tag_mapping`
|
||||
pub fn EnumStringUtils(comptime TargetEnum: type, str_to_tag_mapping: anytype) type {
|
||||
pub fn EnumStringUtils(TargetEnum: anytype, 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,7 +5,10 @@ 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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -31,85 +34,38 @@ fn ErrorDefinitionList(errors: []const ErrorDefinition) type {
|
||||
};
|
||||
}
|
||||
|
||||
// zig fmt: off
|
||||
const ServerUnavailable = ErrorDefinition.init("ServerUnavailable", 503);
|
||||
const RequestFailed = ErrorDefinition.init("RequestFailed", null);
|
||||
const ParseFailed = ErrorDefinition.init("ParseFailed", null);
|
||||
const OutOfMemory = ErrorDefinition.init("OutOfMemory", null);
|
||||
|
||||
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 MapNotFound = ErrorDefinition.init("MapNotFound", 404);
|
||||
const ItemNotFound = ErrorDefinition.init("ItemNotFound", 404);
|
||||
const RecipeNotFound = ErrorDefinition.init("RecipeNotFound", 404);
|
||||
|
||||
// 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 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);
|
||||
|
||||
// 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 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);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
|
||||
pub const FetchError = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
NotAuthenticated,
|
||||
TooManyRequests,
|
||||
FatalError,
|
||||
InvalidPayload,
|
||||
ServerUnavailable,
|
||||
RequestFailed,
|
||||
ParseFailed,
|
||||
@ -118,8 +74,8 @@ pub const FetchError = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
|
||||
const MoveErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
MapNotFound,
|
||||
CharacterLocked,
|
||||
CharacterAlreadyMap,
|
||||
CharacterIsBusy,
|
||||
CharacterAtDestination,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown
|
||||
});
|
||||
@ -127,91 +83,91 @@ pub const MoveError = FetchError || MoveErrorDef.ErrorSet;
|
||||
pub const parseMoveError = MoveErrorDef.parse;
|
||||
|
||||
const FightErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
CharacterLocked,
|
||||
CharacterInventoryFull,
|
||||
CharacterIsBusy,
|
||||
CharacterIsFull,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound,
|
||||
MonsterNotFound,
|
||||
});
|
||||
pub const FightError = FetchError || FightErrorDef.ErrorSet;
|
||||
pub const parseFightError = FightErrorDef.parse;
|
||||
|
||||
const GatherErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
CharacterLocked,
|
||||
CharacterNotSkillLevelRequired,
|
||||
CharacterInventoryFull,
|
||||
CharacterIsBusy,
|
||||
NotEnoughSkill,
|
||||
CharacterIsFull,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
ResourceNotFound
|
||||
});
|
||||
pub const GatherError = FetchError || GatherErrorDef.ErrorSet;
|
||||
pub const parseGatherError = GatherErrorDef.parse;
|
||||
|
||||
const BankDepositItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
NotFound,
|
||||
BankTransactionInProgress,
|
||||
MissingItem,
|
||||
CharacterLocked,
|
||||
ItemNotFound,
|
||||
BankIsBusy,
|
||||
NotEnoughItems,
|
||||
CharacterIsBusy,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
BankNotFound
|
||||
});
|
||||
pub const BankDepositItemError = FetchError || BankDepositItemErrorDef.ErrorSet;
|
||||
pub const parseBankDepositItemError = BankDepositItemErrorDef.parse;
|
||||
|
||||
const BankDepositGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
BankTransactionInProgress,
|
||||
BankInsufficientGold,
|
||||
CharacterLocked,
|
||||
BankIsBusy,
|
||||
NotEnoughGold,
|
||||
CharacterIsBusy,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
BankNotFound
|
||||
});
|
||||
pub const BankDepositGoldError = FetchError || BankDepositGoldErrorDef.ErrorSet;
|
||||
pub const parseBankDepositGoldError = BankDepositGoldErrorDef.parse;
|
||||
|
||||
const BankWithdrawGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
BankTransactionInProgress,
|
||||
BankInsufficientGold,
|
||||
CharacterLocked,
|
||||
BankIsBusy,
|
||||
NotEnoughGold,
|
||||
CharacterIsBusy,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
BankNotFound
|
||||
});
|
||||
pub const BankWithdrawGoldError = FetchError || BankWithdrawGoldErrorDef.ErrorSet;
|
||||
pub const parseBankWithdrawGoldError = BankWithdrawGoldErrorDef.parse;
|
||||
|
||||
const BankWithdrawItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
NotFound,
|
||||
BankTransactionInProgress,
|
||||
MissingItem,
|
||||
CharacterLocked,
|
||||
CharacterInventoryFull,
|
||||
ItemNotFound,
|
||||
BankIsBusy,
|
||||
NotEnoughItems,
|
||||
CharacterIsBusy,
|
||||
CharacterIsFull,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
BankNotFound
|
||||
});
|
||||
pub const BankWithdrawItemError = FetchError || BankWithdrawItemErrorDef.ErrorSet;
|
||||
pub const parseBankWithdrawItemError = BankWithdrawItemErrorDef.parse;
|
||||
|
||||
const CraftErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
NotFound,
|
||||
MissingItem,
|
||||
CharacterLocked,
|
||||
CharacterNotSkillLevelRequired,
|
||||
CharacterInventoryFull,
|
||||
RecipeNotFound,
|
||||
NotEnoughItems,
|
||||
CharacterIsBusy,
|
||||
NotEnoughSkill,
|
||||
CharacterIsFull,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
WorkshopNotFound
|
||||
});
|
||||
pub const CraftError = FetchError || CraftErrorDef.ErrorSet;
|
||||
pub const parseCraftError = CraftErrorDef.parse;
|
||||
|
||||
const UnequipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
NotFound,
|
||||
CharacterLocked,
|
||||
CharacterSlotEquipmentError,
|
||||
CharacterInventoryFull,
|
||||
ItemNotFound, // TODO: Can this really occur? maybe a bug in docs
|
||||
CharacterIsBusy,
|
||||
SlotIsEmpty,
|
||||
CharacterIsFull,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
});
|
||||
@ -219,11 +175,10 @@ pub const UnequipError = FetchError || UnequipErrorDef.ErrorSet;
|
||||
pub const parseUnequipError = UnequipErrorDef.parse;
|
||||
|
||||
const EquipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
NotFound,
|
||||
CharacterItemAlreadyEquiped,
|
||||
CharacterLocked,
|
||||
CharacterSlotEquipmentError,
|
||||
CharacterNotSkillLevelRequired,
|
||||
ItemNotFound,
|
||||
SlotIsFull,
|
||||
CharacterIsBusy,
|
||||
NotEnoughSkill,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
});
|
||||
@ -231,35 +186,23 @@ pub const EquipError = FetchError || EquipErrorDef.ErrorSet;
|
||||
pub const parseEquipError = EquipErrorDef.parse;
|
||||
|
||||
const AcceptTaskErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
CharacterLocked,
|
||||
CharacterIsBusy,
|
||||
AlreadyHasTask,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
TaskMasterNotFound
|
||||
});
|
||||
pub const AcceptTaskError = FetchError || AcceptTaskErrorDef.ErrorSet;
|
||||
pub const parseAcceptTaskError = AcceptTaskErrorDef.parse;
|
||||
|
||||
const TaskCompleteErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||
CharacterLocked,
|
||||
CharacterNoTask,
|
||||
CharacterTaskNotCompleted,
|
||||
CharacterInventoryFull,
|
||||
CharacterIsBusy,
|
||||
HasNoTask,
|
||||
TaskNotCompleted,
|
||||
CharacterIsFull,
|
||||
CharacterNotFound,
|
||||
CharacterInCooldown,
|
||||
MapContentNotFound
|
||||
TaskMasterNotFound
|
||||
});
|
||||
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,15 +19,6 @@ 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;
|
||||
@ -79,6 +70,11 @@ pub fn getStringRequired(object: json.ObjectMap, name: []const u8) ![]const u8 {
|
||||
return getString(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;
|
||||
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;
|
||||
}
|
||||
|
28
api/position.zig
Normal file
28
api/position.zig
Normal file
@ -0,0 +1,28 @@
|
||||
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});
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
// 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,36 +1,17 @@
|
||||
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 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 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");
|
||||
pub const ServerStatus = @import("./schemas/status.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");
|
||||
|
||||
const errors = @import("errors.zig");
|
||||
pub const FetchError = errors.FetchError;
|
||||
@ -42,5 +23,3 @@ 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;
|
||||
|
23
api/schemas/bank_gold_transaction.zig
Normal file
23
api/schemas/bank_gold_transaction.zig
Normal file
@ -0,0 +1,23 @@
|
||||
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)
|
||||
};
|
||||
}
|
23
api/schemas/bank_item_transaction.zig
Normal file
23
api/schemas/bank_item_transaction.zig
Normal file
@ -0,0 +1,23 @@
|
||||
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,162 +1,117 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
pub const Equipment = @import("./equipment.zig");
|
||||
const Store = @import("../store.zig");
|
||||
const Position = @import("../position.zig");
|
||||
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 Task = @import("./task.zig");
|
||||
const SimpleItem = @import("./simple_item.zig");
|
||||
const Position = @import("./position.zig");
|
||||
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
|
||||
|
||||
const Inventory = BoundedSlotsArray(20);
|
||||
|
||||
const Character = @This();
|
||||
|
||||
pub const Skin = enum {
|
||||
men1,
|
||||
men2,
|
||||
men3,
|
||||
women1,
|
||||
women2,
|
||||
women3,
|
||||
|
||||
const Utils = EnumStringUtils(Skin, .{
|
||||
.{ "men1" , Skin.men1 },
|
||||
.{ "men2" , Skin.men2 },
|
||||
.{ "men3" , Skin.men3 },
|
||||
.{ "women1", Skin.women1 },
|
||||
.{ "women2", Skin.women2 },
|
||||
.{ "women3", Skin.women3 },
|
||||
});
|
||||
|
||||
pub const fromString = Utils.fromString;
|
||||
pub const toString = Utils.toString;
|
||||
};
|
||||
|
||||
pub const Skill = enum {
|
||||
combat,
|
||||
fishing,
|
||||
weaponcrafting,
|
||||
gearcrafting,
|
||||
jewelrycrafting,
|
||||
cooking,
|
||||
woodcutting,
|
||||
mining,
|
||||
alchemy,
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Element = enum {
|
||||
water,
|
||||
fire,
|
||||
earth,
|
||||
air,
|
||||
};
|
||||
|
||||
pub const ElementalStats = struct {
|
||||
attack: i64,
|
||||
damage: i64,
|
||||
resistance: i64,
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const TaskMasterTask = struct {
|
||||
target: Store.Id,
|
||||
target_id: Store.CodeId,
|
||||
type: Task.Type,
|
||||
progress: u64,
|
||||
total: u64,
|
||||
|
||||
fn parse(store: *Store, obj: std.json.ObjectMap) !?TaskMasterTask {
|
||||
fn parse(store: *Store, obj: 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.getIntegerRequired(obj, "task_progress");
|
||||
if (progress < 0) {
|
||||
return error.InvalidTaskProgress;
|
||||
}
|
||||
|
||||
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");
|
||||
const total = try json_utils.getIntegerRequired(obj, "task_total");
|
||||
if (total < 0) {
|
||||
return error.InvalidTaskTotal;
|
||||
}
|
||||
|
||||
return TaskMasterTask{
|
||||
.target = try store.tasks.getOrReserveId(task_target),
|
||||
.type = Task.Type.fromString(task_type) orelse return error.InvalidTaskType,
|
||||
.total = total,
|
||||
.progress = progress,
|
||||
.target_id = try store.getCodeId(task_target),
|
||||
.type = Task.TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
|
||||
.total = @intCast(total),
|
||||
.progress = @intCast(progress),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ElementalStatsArray = std.EnumArray(Element, ElementalStats);
|
||||
allocator: Allocator,
|
||||
|
||||
pub const Skills = std.EnumArray(Skill, SkillStats);
|
||||
name: []u8,
|
||||
skin: []u8,
|
||||
account: ?[]u8,
|
||||
gold: i64,
|
||||
hp: i64,
|
||||
haste: i64,
|
||||
position: Position,
|
||||
cooldown_expiration: f64,
|
||||
|
||||
pub const max_name_size = 12;
|
||||
pub const Name = std.BoundedArray(u8, max_name_size);
|
||||
combat: SkillStats,
|
||||
mining: SkillStats,
|
||||
woodcutting: SkillStats,
|
||||
fishing: SkillStats,
|
||||
weaponcrafting: SkillStats,
|
||||
gearcrafting: SkillStats,
|
||||
jewelrycrafting: SkillStats,
|
||||
cooking: SkillStats,
|
||||
|
||||
pub const max_account_size = 32;
|
||||
pub const Account = std.BoundedArray(u8, max_account_size);
|
||||
water: CombatStats,
|
||||
fire: CombatStats,
|
||||
earth: CombatStats,
|
||||
air: CombatStats,
|
||||
|
||||
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,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
|
||||
const name = try json_utils.getStringRequired(obj, "name");
|
||||
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;
|
||||
if (name.len == 0) {
|
||||
return error.InvalidName;
|
||||
}
|
||||
|
||||
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 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 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"),
|
||||
@ -165,40 +120,43 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
|
||||
.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"),
|
||||
});
|
||||
|
||||
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"),
|
||||
});
|
||||
.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 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),
|
||||
.task = try TaskMasterTask.parse(store, obj),
|
||||
.inventory_max_items = inventory_max_items,
|
||||
|
||||
.inventory_max_items = @intCast(inventory_max_items),
|
||||
.inventory = try Inventory.parse(store, inventory),
|
||||
.position = Position.init(x, y),
|
||||
.cooldown_expiration = cooldown_expiration
|
||||
|
||||
.task = task
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
|
||||
return store.characters.appendOrUpdate(try parse(store, obj));
|
||||
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
|
||||
});
|
||||
}
|
||||
|
27
api/schemas/character_fight.zig
Normal file
27
api/schemas/character_fight.zig
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
};
|
||||
}
|
23
api/schemas/character_movement.zig
Normal file
23
api/schemas/character_movement.zig
Normal file
@ -0,0 +1,23 @@
|
||||
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)
|
||||
};
|
||||
}
|
@ -2,23 +2,14 @@ const std = @import("std");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const json = std.json;
|
||||
|
||||
const ElementalStat = @This();
|
||||
|
||||
pub const Element = enum {
|
||||
water,
|
||||
fire,
|
||||
earth,
|
||||
air,
|
||||
};
|
||||
|
||||
pub const Array = std.EnumArray(ElementalStat.Element, ElementalStat);
|
||||
const CombatStats = @This();
|
||||
|
||||
attack: i64,
|
||||
damage: i64,
|
||||
resistance: i64,
|
||||
|
||||
pub fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !ElementalStat {
|
||||
return ElementalStat{
|
||||
pub fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
|
||||
return CombatStats{
|
||||
.attack = try json_utils.getIntegerRequired(object, attack),
|
||||
.damage = try json_utils.getIntegerRequired(object, damage),
|
||||
.resistance = try json_utils.getIntegerRequired(object, resistance),
|
@ -1,12 +1,26 @@
|
||||
// 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;
|
||||
pub 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,
|
||||
@ -14,61 +28,26 @@ pub const Reason = enum {
|
||||
gathering,
|
||||
buy_ge,
|
||||
sell_ge,
|
||||
cancel_ge,
|
||||
delete_item,
|
||||
deposit,
|
||||
withdraw,
|
||||
deposit_gold,
|
||||
withdraw_gold,
|
||||
deposit_bank,
|
||||
withdraw_bank,
|
||||
equip,
|
||||
unequip,
|
||||
task,
|
||||
christmas_exchange,
|
||||
recycling,
|
||||
rest,
|
||||
use,
|
||||
buy_bank_expansion,
|
||||
|
||||
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;
|
||||
const parse = ReasonUtils.fromString;
|
||||
};
|
||||
|
||||
started_at: f64,
|
||||
expiration: f64,
|
||||
reason: 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");
|
||||
pub fn parse(obj: json.ObjectMap) !Cooldown {
|
||||
const reason = try json_utils.getStringRequired(obj, "reason");
|
||||
const expiration = try json_utils.getStringRequired(obj, "expiration");
|
||||
|
||||
return Cooldown{
|
||||
.started_at = parseDateTime(started_at) orelse return error.InvalidStartedAt,
|
||||
.expiration = parseDateTime(expiration) orelse return error.InvalidExpiration,
|
||||
.reason = Reason.fromString(reason) orelse return error.InvalidReason
|
||||
.expiration = parseDateTime(expiration) orelse return error.InvalidDateTime,
|
||||
.reason = Reason.parse(reason) orelse return error.UnknownReason
|
||||
};
|
||||
}
|
||||
|
@ -1,57 +1,22 @@
|
||||
// zig fmt: off
|
||||
const Store = @import("../store.zig");
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const SimpleItem = @import("./simple_item.zig");
|
||||
const Character = @import("./character.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
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 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: std.json.ObjectMap) !Craft {
|
||||
pub fn parse(store: *Store, obj: 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;
|
||||
@ -62,7 +27,7 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Craft {
|
||||
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
|
||||
|
||||
return Craft{
|
||||
.skill = Skill.fromString(skill) orelse return error.InvalidSkill,
|
||||
.skill = SkillUtils.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: Store.Id,
|
||||
item_id: Store.CodeId,
|
||||
rate: u64,
|
||||
min_quantity: u64,
|
||||
max_quantity: u64,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !DropRate {
|
||||
pub fn parse(store: *Store, obj: json.ObjectMap) !DropRate {
|
||||
const rate = try json_utils.getIntegerRequired(obj, "rate");
|
||||
if (rate < 1) {
|
||||
return error.InvalidRate;
|
||||
@ -26,27 +26,33 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !DropRate {
|
||||
return error.InvalidMinQuantity;
|
||||
}
|
||||
|
||||
const code = try json_utils.getStringRequired(obj, "code");
|
||||
const item_id = try store.items.getOrReserveId(code);
|
||||
const code_str = try json_utils.getStringRequired(obj, "code");
|
||||
const item_id = try store.getCodeId(code_str);
|
||||
|
||||
return DropRate{
|
||||
.item = item_id,
|
||||
.item_id = item_id,
|
||||
.rate = @intCast(rate),
|
||||
.min_quantity = @intCast(min_quantity),
|
||||
.max_quantity = @intCast(max_quantity)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseDrops(store: *Store, array: std.json.Array, drops: []DropRate) !usize {
|
||||
for (0.., array.items) |i, drop_value| {
|
||||
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| {
|
||||
const drop_obj = json_utils.asObject(drop_value) orelse return error.InvalidObject;
|
||||
|
||||
if (i >= drops.len) {
|
||||
return error.Overflow;
|
||||
}
|
||||
|
||||
drops[i] = try DropRate.parse(store, drop_obj);
|
||||
try drops.append(try DropRate.parse(store, drop_obj));
|
||||
}
|
||||
|
||||
return array.items.len;
|
||||
return drops;
|
||||
}
|
||||
|
||||
pub fn doesListContain(drops: *DropRates, item_id: Store.CodeId) bool {
|
||||
for (drops.constSlice()) |drop| {
|
||||
if (drop.item_id == item_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
28
api/schemas/equip_request.zig
Normal file
28
api/schemas/equip_request.zig
Normal file
@ -0,0 +1,28 @@
|
||||
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
|
||||
};
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
// 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,39 +1,25 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const Store = @import("../store.zig");
|
||||
const Item = @import("./item.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
const json = std.json;
|
||||
|
||||
const Equipment = @This();
|
||||
|
||||
pub const Slot = struct {
|
||||
item: ?Store.Id = null,
|
||||
quantity: u64 = 0,
|
||||
const CodeId = Store.CodeId;
|
||||
|
||||
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{};
|
||||
}
|
||||
pub const Consumable = struct {
|
||||
code_id: ?CodeId,
|
||||
quantity: i64,
|
||||
|
||||
return Slot{
|
||||
.item = try store.items.getOrReserveId(item_code),
|
||||
.quantity = 1
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
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 SlotId = enum {
|
||||
pub const Slot = enum {
|
||||
weapon,
|
||||
shield,
|
||||
helmet,
|
||||
@ -45,56 +31,61 @@ pub const SlotId = enum {
|
||||
amulet,
|
||||
artifact1,
|
||||
artifact2,
|
||||
artifact3,
|
||||
utility1,
|
||||
utility2,
|
||||
consumable1,
|
||||
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;
|
||||
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",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Slots = std.EnumArray(SlotId, Slot);
|
||||
weapon: ?CodeId,
|
||||
shield: ?CodeId,
|
||||
helmet: ?CodeId,
|
||||
body_armor: ?CodeId,
|
||||
leg_armor: ?CodeId,
|
||||
boots: ?CodeId,
|
||||
|
||||
slots: Slots,
|
||||
ring1: ?CodeId,
|
||||
ring2: ?CodeId,
|
||||
amulet: ?CodeId,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Equipment {
|
||||
artifact1: ?CodeId,
|
||||
artifact2: ?CodeId,
|
||||
artifact3: ?CodeId,
|
||||
|
||||
consumable1: Consumable,
|
||||
consumable2: Consumable,
|
||||
|
||||
pub fn parse(store: *Store, obj: json.ObjectMap) !Equipment {
|
||||
return Equipment{
|
||||
.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"),
|
||||
})
|
||||
.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"),
|
||||
};
|
||||
}
|
||||
|
46
api/schemas/fight.zig
Normal file
46
api/schemas/fight.zig
Normal file
@ -0,0 +1,46 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// 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,14 +1,15 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
const Store = @import("../store.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
|
||||
const Craft = @import("./craft.zig");
|
||||
|
||||
const Item = @This();
|
||||
|
||||
pub const Type = enum {
|
||||
utility,
|
||||
consumable,
|
||||
body_armor,
|
||||
weapon,
|
||||
@ -21,87 +22,51 @@ pub const Type = enum {
|
||||
ring,
|
||||
artifact,
|
||||
currency,
|
||||
|
||||
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 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 },
|
||||
});
|
||||
|
||||
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,
|
||||
name: []u8,
|
||||
code_id: Store.CodeId,
|
||||
level: u64,
|
||||
type: Type,
|
||||
subtype: Subtype,
|
||||
description: Description,
|
||||
subtype: []u8,
|
||||
description: []u8,
|
||||
craft: ?Craft,
|
||||
// TODO: effects
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Item {
|
||||
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Item {
|
||||
const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty;
|
||||
if (level < 1) return error.InvalidLevel;
|
||||
|
||||
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");
|
||||
const craft = json_utils.getObject(obj, "craft");
|
||||
const item_type_str = try json_utils.getStringRequired(obj, "type");
|
||||
|
||||
return Item{
|
||||
.name = try Name.fromSlice(name),
|
||||
.code = try Code.fromSlice(code),
|
||||
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
|
||||
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
|
||||
.level = @intCast(level),
|
||||
.type = Type.fromString(item_type) orelse return error.InvalidType,
|
||||
.subtype = try Subtype.fromSlice(subtype),
|
||||
.description = try Description.fromSlice(description),
|
||||
.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,
|
||||
.craft = if (craft != null) try Craft.parse(store, craft.?) else null
|
||||
};
|
||||
}
|
||||
|
||||
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("}");
|
||||
pub fn deinit(self: Item, allocator: Allocator) void {
|
||||
allocator.free(self.name);
|
||||
allocator.free(self.subtype);
|
||||
allocator.free(self.description);
|
||||
}
|
||||
|
29
api/schemas/item_quantity.zig
Normal file
29
api/schemas/item_quantity.zig
Normal file
@ -0,0 +1,29 @@
|
||||
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,98 +1,34 @@
|
||||
// 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 Monster = @import("./monster.zig");
|
||||
const Resource = @import("./resource.zig");
|
||||
const Craft = @import("./craft.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Map = @This();
|
||||
|
||||
pub const Name = std.BoundedArray(u8, 16);
|
||||
const MapContent = @import("./map_content.zig");
|
||||
|
||||
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,
|
||||
name: []u8,
|
||||
skin: []u8,
|
||||
position: Position,
|
||||
content: ?Content,
|
||||
content: ?MapContent,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Map {
|
||||
_ = store;
|
||||
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Map {
|
||||
const content = json_utils.getObject(obj, "content");
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
const x = json_utils.getInteger(obj, "x") orelse return error.MissingProperty;
|
||||
const y = json_utils.getInteger(obj, "y") orelse return error.MissingProperty;
|
||||
|
||||
return Map{
|
||||
.name = try Name.fromSlice(name),
|
||||
.skin = try Skin.fromSlice(skin),
|
||||
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
|
||||
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
|
||||
.position = Position.init(x, y),
|
||||
.content = content
|
||||
.content = if (content) |c| try MapContent.parse(store, c) else null
|
||||
};
|
||||
}
|
||||
|
||||
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).?.*;
|
||||
pub fn deinit(self: Map, allocator: Allocator) void {
|
||||
allocator.free(self.name);
|
||||
allocator.free(self.skin);
|
||||
}
|
||||
|
37
api/schemas/map_content.zig
Normal file
37
api/schemas/map_content.zig
Normal file
@ -0,0 +1,37 @@
|
||||
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,24 +1,19 @@
|
||||
// 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 DropRate = @import("./drop_rate.zig");
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Element = Character.Element;
|
||||
const DropRate = @import("./drop_rate.zig");
|
||||
const DropRates = DropRate.DropRates;
|
||||
|
||||
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: std.json.ObjectMap, attack: []const u8, resistance: []const u8) !ElementalStats {
|
||||
pub fn parse(object: json.ObjectMap, attack: []const u8, resistance: []const u8) !ElementalStats {
|
||||
return ElementalStats{
|
||||
.attack = try json_utils.getIntegerRequired(object, attack),
|
||||
.resistance = try json_utils.getIntegerRequired(object, resistance),
|
||||
@ -26,50 +21,60 @@ pub const ElementalStats = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const ElementalStatsArray = std.EnumArray(Element, ElementalStats);
|
||||
|
||||
pub const Drops = std.BoundedArray(DropRate, 16);
|
||||
|
||||
name: Name,
|
||||
code: Code,
|
||||
name: []u8,
|
||||
code_id: Store.CodeId,
|
||||
level: u64,
|
||||
hp: u64,
|
||||
elemental_stats: ElementalStatsArray,
|
||||
min_gold: u64,
|
||||
max_gold: u64,
|
||||
drops: Drops,
|
||||
|
||||
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");
|
||||
fire: ElementalStats,
|
||||
earth: ElementalStats,
|
||||
water: ElementalStats,
|
||||
air: ElementalStats,
|
||||
|
||||
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"),
|
||||
});
|
||||
drops: DropRates,
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
|
||||
return Monster{
|
||||
.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
|
||||
.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)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
|
||||
return store.monsters.appendOrUpdate(try parse(store, obj));
|
||||
pub fn deinit(self: Monster, allocator: Allocator) void {
|
||||
allocator.free(self.name);
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
// 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;
|
||||
}
|
@ -1,39 +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 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,10 +1,12 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const Character = @import("./character.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const DropRate = @import("./drop_rate.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
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 Resource = @This();
|
||||
|
||||
@ -12,62 +14,38 @@ pub const Skill = enum {
|
||||
mining,
|
||||
woodcutting,
|
||||
fishing,
|
||||
alchemy,
|
||||
|
||||
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 SkillUtils = EnumStringUtils(Skill, .{
|
||||
.{ "mining" , .mining },
|
||||
.{ "woodcutting", .woodcutting },
|
||||
.{ "fishing" , .fishing },
|
||||
});
|
||||
|
||||
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,
|
||||
name: []u8,
|
||||
code_id: Store.CodeId,
|
||||
skill: Skill,
|
||||
level: u64,
|
||||
drops: Drops,
|
||||
drops: DropRates,
|
||||
|
||||
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");
|
||||
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Resource {
|
||||
const drops_array = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
|
||||
|
||||
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));
|
||||
const level = try json_utils.getIntegerRequired(obj, "level");
|
||||
if (level < 0) {
|
||||
return error.InvalidLevel;
|
||||
}
|
||||
|
||||
const skill_str = try json_utils.getStringRequired(obj, "skill");
|
||||
|
||||
return Resource{
|
||||
.name = try Name.fromSlice(name),
|
||||
.code = try Code.fromSlice(code),
|
||||
.level = level,
|
||||
.skill = Skill.fromString(skill) orelse return error.InvalidSkill,
|
||||
.drops = drops
|
||||
.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)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
|
||||
return store.resources.appendOrUpdate(try parse(store, obj));
|
||||
pub fn deinit(self: Resource, allocator: Allocator) void {
|
||||
allocator.free(self.name);
|
||||
}
|
||||
|
@ -1,152 +0,0 @@
|
||||
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("}");
|
||||
}
|
||||
};
|
||||
}
|
22
api/schemas/single_item.zig
Normal file
22
api/schemas/single_item.zig
Normal file
@ -0,0 +1,22 @@
|
||||
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),
|
||||
};
|
||||
}
|
18
api/schemas/skill.zig
Normal file
18
api/schemas/skill.zig
Normal file
@ -0,0 +1,18 @@
|
||||
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 },
|
||||
});
|
27
api/schemas/skill_data.zig
Normal file
27
api/schemas/skill_data.zig
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
};
|
||||
}
|
25
api/schemas/skill_info.zig
Normal file
25
api/schemas/skill_info.zig
Normal file
@ -0,0 +1,25 @@
|
||||
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),
|
||||
};
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
// 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)
|
||||
};
|
||||
}
|
17
api/schemas/skill_stats.zig
Normal file
17
api/schemas/skill_stats.zig
Normal file
@ -0,0 +1,17 @@
|
||||
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),
|
||||
};
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// 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;
|
||||
}
|
108
api/schemas/slot_array.zig
Normal file
108
api/schemas/slot_array.zig
Normal file
@ -0,0 +1,108 @@
|
||||
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,102 +1,32 @@
|
||||
// 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();
|
||||
|
||||
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);
|
||||
|
||||
message: Message,
|
||||
created_at: f64
|
||||
};
|
||||
pub const Announcements = std.BoundedArray(Announcement, 16);
|
||||
|
||||
status: Status,
|
||||
version: Version,
|
||||
allocator: Allocator,
|
||||
status: []const u8,
|
||||
version: []const u8,
|
||||
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 {
|
||||
pub fn parse(store: *Store, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
|
||||
_ = store;
|
||||
|
||||
const characters_online = json_utils.getInteger(obj, "characters_online") orelse return error.MissingProperty;
|
||||
const characters_online = json_utils.getInteger(object, "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 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)
|
||||
.status = (try json_utils.dupeString(allocator, object, "status")) orelse return error.MissingStatus,
|
||||
.version = (try json_utils.dupeString(allocator, object, "version")) orelse return error.MissingVersion
|
||||
};
|
||||
}
|
||||
|
||||
pub fn 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("}");
|
||||
pub fn deinit(self: ServerStatus) void {
|
||||
self.allocator.free(self.status);
|
||||
self.allocator.free(self.version);
|
||||
}
|
||||
|
@ -1,45 +1,36 @@
|
||||
// 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,
|
||||
|
||||
const Utils = EnumStringUtils(Type, .{
|
||||
.{ "monsters" , Type.monsters },
|
||||
.{ "resources", Type.resources },
|
||||
.{ "crafts" , Type.crafts },
|
||||
});
|
||||
pub const fromString = Utils.fromString;
|
||||
pub const toString = Utils.toString;
|
||||
crafts
|
||||
};
|
||||
pub const TypeUtils = EnumStringUtils(Type, .{
|
||||
.{ "monsters" , Type.monsters },
|
||||
.{ "resources", Type.resources },
|
||||
.{ "crafts" , Type.crafts },
|
||||
});
|
||||
|
||||
pub const Code = std.BoundedArray(u8, 32);
|
||||
|
||||
code: Code,
|
||||
code_id: Store.CodeId,
|
||||
type: Type,
|
||||
total: u64,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Task {
|
||||
_ = store;
|
||||
const code = try json_utils.getStringRequired(obj, "code");
|
||||
pub fn parse(store: *Store, obj: json.ObjectMap) !Task {
|
||||
const task_type = try json_utils.getStringRequired(obj, "type");
|
||||
const total = try json_utils.getPositiveIntegerRequired(obj, "total");
|
||||
const total = try json_utils.getIntegerRequired(obj, "total");
|
||||
if (total < 0) {
|
||||
return error.InvalidTaskTotal;
|
||||
}
|
||||
|
||||
return Task{
|
||||
.code = try Code.fromSlice(code),
|
||||
.type = Type.fromString(task_type) orelse return error.InvalidTaskType,
|
||||
.total = total
|
||||
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
|
||||
.type = TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
|
||||
.total = @intCast(total)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
|
||||
return store.tasks.appendOrUpdate(try parse(store, obj));
|
||||
}
|
||||
|
27
api/schemas/task_data.zig
Normal file
27
api/schemas/task_data.zig
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
};
|
||||
}
|
27
api/schemas/task_reward_data.zig
Normal file
27
api/schemas/task_reward_data.zig
Normal file
@ -0,0 +1,27 @@
|
||||
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
|
||||
};
|
||||
}
|
1575
api/server.zig
1575
api/server.zig
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
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] };
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
#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);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
966
api/store.zig
966
api/store.zig
File diff suppressed because it is too large
Load Diff
@ -22,10 +22,6 @@ 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"));
|
||||
}
|
||||
|
||||
@ -86,10 +82,8 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
gui.linkLibrary(raylib_dep.artifact("raylib"));
|
||||
|
||||
gui.root_module.addImport("artifacts-api", api);
|
||||
gui.root_module.addImport("artificer", lib);
|
||||
gui.linkLibrary(raylib_dep.artifact("raylib"));
|
||||
gui.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
||||
|
||||
const run_cmd = b.addRunArtifact(gui);
|
||||
|
71
cli/main.zig
71
cli/main.zig
@ -1,4 +1,3 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
@ -6,15 +5,6 @@ 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);
|
||||
@ -28,7 +18,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 {
|
||||
@ -39,54 +29,25 @@ pub fn main() !void {
|
||||
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
|
||||
defer allocator.free(token);
|
||||
|
||||
var store = try Api.Store.init(allocator);
|
||||
defer store.deinit(allocator);
|
||||
var artificer = try Artificer.init(allocator, token);
|
||||
defer artificer.deinit();
|
||||
|
||||
var server = try Api.Server.init(allocator, &store);
|
||||
defer server.deinit();
|
||||
|
||||
try server.setToken(token);
|
||||
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
|
||||
defer allocator.free(cache_path);
|
||||
|
||||
std.log.info("Prefetching server data", .{});
|
||||
{
|
||||
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-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.server.prefetchCached(cache_path);
|
||||
|
||||
if (false) {
|
||||
std.log.info("Starting main loop", .{});
|
||||
const started_at = artificer.clock.nanoTimestamp();
|
||||
try artificer.runUntilGoalsComplete();
|
||||
const stopped_at = artificer.clock.nanoTimestamp();
|
||||
while (true) {
|
||||
const waitUntil = artificer.nextStepAt();
|
||||
const duration = waitUntil - std.time.milliTimestamp();
|
||||
if (duration > 0) {
|
||||
std.time.sleep(@intCast(duration));
|
||||
}
|
||||
|
||||
const elapsed_time = @as(f64, @floatFromInt(stopped_at - started_at)) / std.time.ns_per_s;
|
||||
std.log.info("Took {d:.3}s", .{ elapsed_time });
|
||||
try artificer.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
698
gui/app.zig
698
gui/app.zig
@ -1,698 +0,0 @@
|
||||
// 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
24
gui/base.vsh
@ -1,24 +0,0 @@
|
||||
#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
45
gui/blur.fsh
@ -1,45 +0,0 @@
|
||||
#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;
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
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,64 +1,15 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Api = @import("artifacts-api");
|
||||
const Artificer = @import("artificer");
|
||||
const rl = @import("raylib");
|
||||
const raylib_h = @cImport({
|
||||
@cInclude("stdio.h");
|
||||
@cInclude("raylib.h");
|
||||
});
|
||||
const App = @import("./app.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const std_options = .{
|
||||
.log_scope_levels = &[_]std.log.ScopeLevel{
|
||||
.{ .scope = .api, .level = .warn },
|
||||
.{ .scope = .raylib, .level = .warn },
|
||||
}
|
||||
};
|
||||
const srcery = @import("./srcery.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,
|
||||
};
|
||||
}
|
||||
const UI = @import("./ui.zig");
|
||||
const UIStack = @import("./ui_stack.zig");
|
||||
const RectUtils = @import("./rect_utils.zig");
|
||||
|
||||
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 {
|
||||
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
@ -71,7 +22,37 @@ fn getAPITokenFromArgs(allocator: std.mem.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 "));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
@ -79,44 +60,45 @@ 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 store = try Api.Store.init(allocator);
|
||||
defer store.deinit(allocator);
|
||||
var artificer = try Artificer.init(allocator, token);
|
||||
defer artificer.deinit();
|
||||
|
||||
var server = try Api.Server.init(allocator, &store);
|
||||
defer server.deinit();
|
||||
|
||||
try server.setToken(token);
|
||||
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
|
||||
defer allocator.free(cache_path);
|
||||
|
||||
std.log.info("Prefetching server data", .{});
|
||||
{
|
||||
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 });
|
||||
}
|
||||
try artificer.server.prefetchCached(cache_path);
|
||||
|
||||
rl.initWindow(800, 450, "Artificer");
|
||||
defer rl.closeWindow();
|
||||
|
||||
rl.setWindowMinSize(200, 200);
|
||||
rl.setWindowState(.{
|
||||
.vsync_hint = true,
|
||||
.window_resizable = true
|
||||
});
|
||||
rl.setTargetFPS(60);
|
||||
|
||||
var app = try App.init(allocator, &store, &server);
|
||||
defer app.deinit();
|
||||
var ui = UI.init();
|
||||
defer ui.deinit();
|
||||
|
||||
while (!rl.windowShouldClose()) {
|
||||
try app.tick();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
321
gui/ui.zig
321
gui/ui.zig
@ -1,200 +1,159 @@
|
||||
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 std = @import("std");
|
||||
|
||||
const UI = @This();
|
||||
|
||||
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),
|
||||
font: rl.Font,
|
||||
|
||||
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{
|
||||
.transform_stack = stack
|
||||
.font = rl.getFontDefault()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isHot(self: *const UI, id: Id) bool {
|
||||
if (self.hot_widget) |hot_id| {
|
||||
return hot_id.eql(id);
|
||||
}
|
||||
return false;
|
||||
pub fn deinit(self: UI) void {
|
||||
rl.unloadFont(self.font);
|
||||
}
|
||||
|
||||
pub fn isActive(self: *const UI, id: Id) bool {
|
||||
if (self.active_widget) |active_id| {
|
||||
return active_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 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;
|
||||
}
|
||||
var fallbackIndex: usize = 0; // Get index of fallback glyph '?'
|
||||
|
||||
fn getTopFrame(self: *UI) *TransformFrame {
|
||||
assert(self.transform_stack.len >= 1);
|
||||
return &self.transform_stack.buffer[self.transform_stack.len-1];
|
||||
}
|
||||
for (0..@intCast(font.glyphCount), font.glyphs) |i, glyph| {
|
||||
if (glyph.value == '?') fallbackIndex = i;
|
||||
|
||||
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 (glyph.value == codepoint)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eql(a: Id, b: Id) bool {
|
||||
return a.location == b.location and a.extra == b.extra;
|
||||
}
|
||||
};
|
||||
if ((index == 0) and (font.glyphs[0].value != codepoint)) index = fallbackIndex;
|
||||
|
||||
pub const Stack = struct {
|
||||
pub const Direction = enum {
|
||||
top_to_bottom,
|
||||
bottom_to_top,
|
||||
left_to_right
|
||||
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();
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
drawTextEx(font, text, adjusted_position, font_size, spacing, color);
|
||||
}
|
||||
|
42
gui/ui_stack.zig
Normal file
42
gui/ui_stack.zig
Normal file
@ -0,0 +1,42 @@
|
||||
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
Normal file
183
lib/action.zig
Normal file
@ -0,0 +1,183 @@
|
||||
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);
|
||||
}
|
@ -1,457 +0,0 @@
|
||||
// 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
Normal file
104
lib/brain.zig
Normal file
@ -0,0 +1,104 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
// 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,11 +1,136 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Api = @import("artifacts-api");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
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");
|
||||
pub const Brain = @import("./brain.zig");
|
||||
pub const TaskGraph = @import("./task_graph.zig");
|
||||
|
||||
pub const ArtificerApi = ArtificerType(SystemClock, Api.Server);
|
||||
pub const ArtificerSim = ArtificerType(SimClock, SimServer);
|
||||
const Artificer = @This();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
// 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;
|
||||
}
|
@ -1,227 +0,0 @@
|
||||
// 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;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
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
Normal file
302
lib/task.zig
Normal file
@ -0,0 +1,302 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
197
lib/task_graph.zig
Normal file
197
lib/task_graph.zig
Normal file
@ -0,0 +1,197 @@
|
||||
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