Compare commits
9 Commits
ee563e6ba6
...
f5a4ba4503
| Author | SHA1 | Date | |
|---|---|---|---|
| f5a4ba4503 | |||
| 429b1dcd6e | |||
| cc289194a8 | |||
| fdbf005126 | |||
| dad53513c7 | |||
| f9dc023b90 | |||
| 45e32424cb | |||
| 64c4d9ff47 | |||
| 7d572e7064 |
@ -1,5 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
// TODO: Maybe it would be good to move date time parsing to separate module
|
||||||
|
|
||||||
pub fn parseDateTime(datetime: []const u8) ?f64 {
|
pub fn parseDateTime(datetime: []const u8) ?f64 {
|
||||||
const time_h = @cImport({
|
const time_h = @cImport({
|
||||||
@cDefine("_XOPEN_SOURCE", "700");
|
@cDefine("_XOPEN_SOURCE", "700");
|
||||||
25
api/enum_string_utils.zig
Normal file
25
api/enum_string_utils.zig
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
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.ComptimeStringMap(TargetEnum, str_to_tag_mapping);
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
pub fn fromString(str: []const u8) ?TargetEnum {
|
||||||
|
return EnumMapping.get(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(value: TargetEnum) []const u8 {
|
||||||
|
inline for (str_to_tag_mapping) |mapping| {
|
||||||
|
if (mapping[1] == value) {
|
||||||
|
return mapping[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
208
api/errors.zig
Normal file
208
api/errors.zig
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const ErrorDefinition = struct {
|
||||||
|
name: [:0]const u8,
|
||||||
|
code: ?u10,
|
||||||
|
|
||||||
|
fn init(name: [:0]const u8, code: ?u10) ErrorDefinition {
|
||||||
|
return ErrorDefinition{
|
||||||
|
.name = name,
|
||||||
|
.code = code
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn ErrorDefinitionList(errors: []const ErrorDefinition) type {
|
||||||
|
var errorNames: [errors.len]std.builtin.Type.Error = undefined;
|
||||||
|
for (0.., errors) |i, def| {
|
||||||
|
errorNames[i] = .{ .name = def.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
const error_set = @Type(.{ .ErrorSet = &errorNames });
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
const ErrorSet = error_set;
|
||||||
|
|
||||||
|
fn parse(status: std.http.Status) ?ErrorSet {
|
||||||
|
inline for (errors) |err| {
|
||||||
|
if (err.code == @intFromEnum(status)) {
|
||||||
|
return @field(ErrorSet, err.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerUnavailable = ErrorDefinition.init("ServerUnavailable", 503);
|
||||||
|
const RequestFailed = ErrorDefinition.init("RequestFailed", null);
|
||||||
|
const ParseFailed = ErrorDefinition.init("ParseFailed", null);
|
||||||
|
const OutOfMemory = ErrorDefinition.init("OutOfMemory", null);
|
||||||
|
|
||||||
|
const MapNotFound = ErrorDefinition.init("MapNotFound", 404);
|
||||||
|
const ItemNotFound = ErrorDefinition.init("ItemNotFound", 404);
|
||||||
|
const RecipeNotFound = ErrorDefinition.init("RecipeNotFound", 404);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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{
|
||||||
|
ServerUnavailable,
|
||||||
|
RequestFailed,
|
||||||
|
ParseFailed,
|
||||||
|
OutOfMemory,
|
||||||
|
}).ErrorSet;
|
||||||
|
|
||||||
|
const MoveErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
MapNotFound,
|
||||||
|
CharacterIsBusy,
|
||||||
|
CharacterAtDestination,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown
|
||||||
|
});
|
||||||
|
pub const MoveError = FetchError || MoveErrorDef.ErrorSet;
|
||||||
|
pub const parseMoveError = MoveErrorDef.parse;
|
||||||
|
|
||||||
|
const FightErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
CharacterIsBusy,
|
||||||
|
CharacterIsFull,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
MonsterNotFound,
|
||||||
|
});
|
||||||
|
pub const FightError = FetchError || FightErrorDef.ErrorSet;
|
||||||
|
pub const parseFightError = FightErrorDef.parse;
|
||||||
|
|
||||||
|
const GatherErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
CharacterIsBusy,
|
||||||
|
NotEnoughSkill,
|
||||||
|
CharacterIsFull,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
ResourceNotFound
|
||||||
|
});
|
||||||
|
pub const GatherError = FetchError || GatherErrorDef.ErrorSet;
|
||||||
|
pub const parseGatherError = GatherErrorDef.parse;
|
||||||
|
|
||||||
|
const BankDepositItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
ItemNotFound,
|
||||||
|
BankIsBusy,
|
||||||
|
NotEnoughItems,
|
||||||
|
CharacterIsBusy,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
BankNotFound
|
||||||
|
});
|
||||||
|
pub const BankDepositItemError = FetchError || BankDepositItemErrorDef.ErrorSet;
|
||||||
|
pub const parseBankDepositItemError = BankDepositItemErrorDef.parse;
|
||||||
|
|
||||||
|
const BankDepositGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
BankIsBusy,
|
||||||
|
NotEnoughGold,
|
||||||
|
CharacterIsBusy,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
BankNotFound
|
||||||
|
});
|
||||||
|
pub const BankDepositGoldError = FetchError || BankDepositGoldErrorDef.ErrorSet;
|
||||||
|
pub const parseBankDepositGoldError = BankDepositGoldErrorDef.parse;
|
||||||
|
|
||||||
|
const BankWithdrawGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
BankIsBusy,
|
||||||
|
NotEnoughGold,
|
||||||
|
CharacterIsBusy,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
BankNotFound
|
||||||
|
});
|
||||||
|
pub const BankWithdrawGoldError = FetchError || BankWithdrawGoldErrorDef.ErrorSet;
|
||||||
|
pub const parseBankWithdrawGoldError = BankWithdrawGoldErrorDef.parse;
|
||||||
|
|
||||||
|
const BankWithdrawItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
ItemNotFound,
|
||||||
|
BankIsBusy,
|
||||||
|
NotEnoughItems,
|
||||||
|
CharacterIsBusy,
|
||||||
|
CharacterIsFull,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
BankNotFound
|
||||||
|
});
|
||||||
|
pub const BankWithdrawItemError = FetchError || BankWithdrawItemErrorDef.ErrorSet;
|
||||||
|
pub const parseBankWithdrawItemError = BankWithdrawItemErrorDef.parse;
|
||||||
|
|
||||||
|
const CraftErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
RecipeNotFound,
|
||||||
|
NotEnoughItems,
|
||||||
|
CharacterIsBusy,
|
||||||
|
NotEnoughSkill,
|
||||||
|
CharacterIsFull,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
WorkshopNotFound
|
||||||
|
});
|
||||||
|
pub const CraftError = FetchError || CraftErrorDef.ErrorSet;
|
||||||
|
pub const parseCraftError = CraftErrorDef.parse;
|
||||||
|
|
||||||
|
const UnequipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
ItemNotFound, // TODO: Can this really occur? maybe a bug in docs
|
||||||
|
CharacterIsBusy,
|
||||||
|
SlotIsEmpty,
|
||||||
|
CharacterIsFull,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
});
|
||||||
|
pub const UnequipError = FetchError || UnequipErrorDef.ErrorSet;
|
||||||
|
pub const parseUnequipError = UnequipErrorDef.parse;
|
||||||
|
|
||||||
|
const EquipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
ItemNotFound,
|
||||||
|
SlotIsFull,
|
||||||
|
CharacterIsBusy,
|
||||||
|
NotEnoughSkill,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
});
|
||||||
|
pub const EquipError = FetchError || EquipErrorDef.ErrorSet;
|
||||||
|
pub const parseEquipError = EquipErrorDef.parse;
|
||||||
|
|
||||||
|
const AcceptTaskErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
CharacterIsBusy,
|
||||||
|
AlreadyHasTask,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
TaskMasterNotFound
|
||||||
|
});
|
||||||
|
pub const AcceptTaskError = FetchError || AcceptTaskErrorDef.ErrorSet;
|
||||||
|
pub const parseAcceptTaskError = AcceptTaskErrorDef.parse;
|
||||||
|
|
||||||
|
const TaskCompleteErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
|
||||||
|
CharacterIsBusy,
|
||||||
|
HasNoTask,
|
||||||
|
TaskNotCompleted,
|
||||||
|
CharacterIsFull,
|
||||||
|
CharacterNotFound,
|
||||||
|
CharacterInCooldown,
|
||||||
|
TaskMasterNotFound
|
||||||
|
});
|
||||||
|
pub const TaskCompleteError = FetchError || TaskCompleteErrorDef.ErrorSet;
|
||||||
|
pub const parseTaskCompleteError = TaskCompleteErrorDef.parse;
|
||||||
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});
|
||||||
|
}
|
||||||
25
api/root.zig
Normal file
25
api/root.zig
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime;
|
||||||
|
|
||||||
|
pub const Server = @import("server.zig");
|
||||||
|
pub const Store = @import("store.zig");
|
||||||
|
pub const Character = @import("./schemas/character.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;
|
||||||
|
pub const MoveError = errors.MoveError;
|
||||||
|
pub const FightError = errors.FightError;
|
||||||
|
pub const GatherError = errors.GatherError;
|
||||||
|
pub const BankDepositGoldError = errors.BankDepositGoldError;
|
||||||
|
pub const BankDepositItemError = errors.BankDepositItemError;
|
||||||
|
pub const BankWithdrawItemError = errors.BankWithdrawItemError;
|
||||||
|
pub const CraftError = errors.CraftError;
|
||||||
|
pub const AcceptTaskError = errors.AcceptTaskError;
|
||||||
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,9 +1,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("../json_utils.zig");
|
||||||
const Server = @import("./server.zig");
|
const Store = @import("../store.zig");
|
||||||
const Position = @import("./position.zig");
|
const Position = @import("../position.zig");
|
||||||
const parseDateTime = Server.parseDateTime;
|
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
|
||||||
const ItemId = Server.ItemId;
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const json = std.json;
|
const json = std.json;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
@ -11,12 +10,42 @@ const assert = std.debug.assert;
|
|||||||
const SkillStats = @import("./skill_stats.zig");
|
const SkillStats = @import("./skill_stats.zig");
|
||||||
const CombatStats = @import("./combat_stats.zig");
|
const CombatStats = @import("./combat_stats.zig");
|
||||||
const Equipment = @import("./equipment.zig");
|
const Equipment = @import("./equipment.zig");
|
||||||
|
const Task = @import("./task.zig");
|
||||||
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
|
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
|
||||||
|
|
||||||
const Inventory = BoundedSlotsArray(20);
|
const Inventory = BoundedSlotsArray(20);
|
||||||
|
|
||||||
const Character = @This();
|
const Character = @This();
|
||||||
|
|
||||||
|
const TaskMasterTask = struct {
|
||||||
|
target_id: Store.CodeId,
|
||||||
|
type: Task.Type,
|
||||||
|
progress: u64,
|
||||||
|
total: u64,
|
||||||
|
|
||||||
|
fn parse(store: *Store, obj: json.ObjectMap) !TaskMasterTask {
|
||||||
|
const task_target = try json_utils.getStringRequired(obj, "task");
|
||||||
|
const task_type = try json_utils.getStringRequired(obj, "task_type");
|
||||||
|
|
||||||
|
const progress = try json_utils.getIntegerRequired(obj, "task_progress");
|
||||||
|
if (progress < 0) {
|
||||||
|
return error.InvalidTaskProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = try json_utils.getIntegerRequired(obj, "task_total");
|
||||||
|
if (total < 0) {
|
||||||
|
return error.InvalidTaskTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskMasterTask{
|
||||||
|
.target_id = try store.getCodeId(task_target),
|
||||||
|
.type = Task.TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
|
||||||
|
.total = @intCast(total),
|
||||||
|
.progress = @intCast(progress),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
name: []u8,
|
name: []u8,
|
||||||
@ -44,17 +73,32 @@ air: CombatStats,
|
|||||||
|
|
||||||
equipment: Equipment,
|
equipment: Equipment,
|
||||||
|
|
||||||
inventory_max_items: i64,
|
inventory_max_items: u64,
|
||||||
inventory: Inventory,
|
inventory: Inventory,
|
||||||
|
|
||||||
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character {
|
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 inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty;
|
||||||
const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") 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 x = try json_utils.getIntegerRequired(obj, "x");
|
||||||
const y = try json_utils.getIntegerRequired(obj, "y");
|
const y = try json_utils.getIntegerRequired(obj, "y");
|
||||||
const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty;
|
const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty;
|
||||||
assert(name.len > 0);
|
if (name.len == 0) {
|
||||||
|
return error.InvalidName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty;
|
||||||
|
if (inventory_max_items < 0) {
|
||||||
|
return error.InvalidInventoryMaxItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: ?TaskMasterTask = null;
|
||||||
|
const task_target = try json_utils.getStringRequired(obj, "task");
|
||||||
|
if (task_target.len > 0) {
|
||||||
|
task = try TaskMasterTask.parse(store, obj);
|
||||||
|
}
|
||||||
|
|
||||||
return Character{
|
return Character{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
@ -82,10 +126,12 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character
|
|||||||
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
|
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
|
||||||
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
|
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
|
||||||
|
|
||||||
.equipment = try Equipment.parse(api, obj),
|
.equipment = try Equipment.parse(store, obj),
|
||||||
|
|
||||||
.inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
|
.inventory_max_items = @intCast(inventory_max_items),
|
||||||
.inventory = try Inventory.parse(api, inventory)
|
.inventory = try Inventory.parse(store, inventory),
|
||||||
|
|
||||||
|
.task = task
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("../json_utils.zig");
|
||||||
const json = std.json;
|
const json = std.json;
|
||||||
|
|
||||||
const CombatStats = @This();
|
const CombatStats = @This();
|
||||||
53
api/schemas/cooldown.zig
Normal file
53
api/schemas/cooldown.zig
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
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,
|
||||||
|
crafting,
|
||||||
|
gathering,
|
||||||
|
buy_ge,
|
||||||
|
sell_ge,
|
||||||
|
delete_item,
|
||||||
|
deposit_bank,
|
||||||
|
withdraw_bank,
|
||||||
|
equip,
|
||||||
|
unequip,
|
||||||
|
task,
|
||||||
|
recycling,
|
||||||
|
|
||||||
|
const parse = ReasonUtils.fromString;
|
||||||
|
};
|
||||||
|
|
||||||
|
expiration: f64,
|
||||||
|
reason: Reason,
|
||||||
|
|
||||||
|
pub fn parse(obj: json.ObjectMap) !Cooldown {
|
||||||
|
const reason = try json_utils.getStringRequired(obj, "reason");
|
||||||
|
const expiration = try json_utils.getStringRequired(obj, "expiration");
|
||||||
|
|
||||||
|
return Cooldown{
|
||||||
|
.expiration = parseDateTime(expiration) orelse return error.InvalidDateTime,
|
||||||
|
.reason = Reason.parse(reason) orelse return error.UnknownReason
|
||||||
|
};
|
||||||
|
}
|
||||||
35
api/schemas/craft.zig
Normal file
35
api/schemas/craft.zig
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Store = @import("../store.zig");
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const Skill = @import("./skill.zig").Skill;
|
||||||
|
const SkillUtils = @import("./skill.zig").SkillUtils;
|
||||||
|
|
||||||
|
const Items = BoundedSlotsArray(8);
|
||||||
|
|
||||||
|
const Craft = @This();
|
||||||
|
|
||||||
|
skill: Skill,
|
||||||
|
level: u64,
|
||||||
|
quantity: u64,
|
||||||
|
items: Items,
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const quantity = json_utils.getInteger(obj, "quantity") orelse return error.MissingProperty;
|
||||||
|
if (quantity < 1) return error.InvalidQuantity;
|
||||||
|
|
||||||
|
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
|
||||||
|
|
||||||
|
return Craft{
|
||||||
|
.skill = SkillUtils.fromString(skill) orelse return error.InvalidSkill,
|
||||||
|
.level = @intCast(level),
|
||||||
|
.quantity = @intCast(quantity),
|
||||||
|
.items = try Items.parse(store, items)
|
||||||
|
};
|
||||||
|
}
|
||||||
58
api/schemas/drop_rate.zig
Normal file
58
api/schemas/drop_rate.zig
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Store = @import("../store.zig");
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const DropRate = @This();
|
||||||
|
|
||||||
|
item_id: Store.CodeId,
|
||||||
|
rate: u64,
|
||||||
|
min_quantity: u64,
|
||||||
|
max_quantity: u64,
|
||||||
|
|
||||||
|
pub fn parse(store: *Store, obj: json.ObjectMap) !DropRate {
|
||||||
|
const rate = try json_utils.getIntegerRequired(obj, "rate");
|
||||||
|
if (rate < 1) {
|
||||||
|
return error.InvalidRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min_quantity = try json_utils.getIntegerRequired(obj, "min_quantity");
|
||||||
|
if (min_quantity < 1) {
|
||||||
|
return error.InvalidMinQuantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const max_quantity = try json_utils.getIntegerRequired(obj, "max_quantity");
|
||||||
|
if (max_quantity < 1) {
|
||||||
|
return error.InvalidMinQuantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code_str = try json_utils.getStringRequired(obj, "code");
|
||||||
|
const item_id = try store.getCodeId(code_str);
|
||||||
|
|
||||||
|
return DropRate{
|
||||||
|
.item_id = item_id,
|
||||||
|
.rate = @intCast(rate),
|
||||||
|
.min_quantity = @intCast(min_quantity),
|
||||||
|
.max_quantity = @intCast(max_quantity)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DropRates = std.BoundedArray(DropRate, 8); // TODO: Maybe rename to "List"?
|
||||||
|
|
||||||
|
pub fn parseList(store: *Store, array: json.Array) !DropRates {
|
||||||
|
var drops = DropRates.init(0) catch unreachable;
|
||||||
|
for (array.items) |drop_value| {
|
||||||
|
const drop_obj = json_utils.asObject(drop_value) orelse return error.InvalidObject;
|
||||||
|
try drops.append(try DropRate.parse(store, drop_obj));
|
||||||
|
}
|
||||||
|
return drops;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn doesListContain(drops: *DropRates, item_id: Store.CodeId) bool {
|
||||||
|
for (drops.constSlice()) |drop| {
|
||||||
|
if (drop.item_id == item_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
91
api/schemas/equipment.zig
Normal file
91
api/schemas/equipment.zig
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
const Store = @import("../store.zig");
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const Equipment = @This();
|
||||||
|
|
||||||
|
const CodeId = Store.CodeId;
|
||||||
|
|
||||||
|
pub const Consumable = struct {
|
||||||
|
code_id: ?CodeId,
|
||||||
|
quantity: i64,
|
||||||
|
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Slot = enum {
|
||||||
|
weapon,
|
||||||
|
shield,
|
||||||
|
helmet,
|
||||||
|
body_armor,
|
||||||
|
leg_armor,
|
||||||
|
boots,
|
||||||
|
ring1,
|
||||||
|
ring2,
|
||||||
|
amulet,
|
||||||
|
artifact1,
|
||||||
|
artifact2,
|
||||||
|
consumable1,
|
||||||
|
consumable2,
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
weapon: ?CodeId,
|
||||||
|
shield: ?CodeId,
|
||||||
|
helmet: ?CodeId,
|
||||||
|
body_armor: ?CodeId,
|
||||||
|
leg_armor: ?CodeId,
|
||||||
|
boots: ?CodeId,
|
||||||
|
|
||||||
|
ring1: ?CodeId,
|
||||||
|
ring2: ?CodeId,
|
||||||
|
amulet: ?CodeId,
|
||||||
|
|
||||||
|
artifact1: ?CodeId,
|
||||||
|
artifact2: ?CodeId,
|
||||||
|
artifact3: ?CodeId,
|
||||||
|
|
||||||
|
consumable1: Consumable,
|
||||||
|
consumable2: Consumable,
|
||||||
|
|
||||||
|
pub fn parse(store: *Store, obj: json.ObjectMap) !Equipment {
|
||||||
|
return Equipment{
|
||||||
|
.weapon = try store.getCodeIdJson(obj, "weapon_slot"),
|
||||||
|
.shield = try store.getCodeIdJson(obj, "shield_slot"),
|
||||||
|
.helmet = try store.getCodeIdJson(obj, "helmet_slot"),
|
||||||
|
.body_armor = try store.getCodeIdJson(obj, "body_armor_slot"),
|
||||||
|
.leg_armor = try store.getCodeIdJson(obj, "leg_armor_slot"),
|
||||||
|
.boots = try store.getCodeIdJson(obj, "boots_slot"),
|
||||||
|
.ring1 = try store.getCodeIdJson(obj, "ring1_slot"),
|
||||||
|
.ring2 = try store.getCodeIdJson(obj, "ring2_slot"),
|
||||||
|
.amulet = try store.getCodeIdJson(obj, "amulet_slot"),
|
||||||
|
.artifact1 = try store.getCodeIdJson(obj, "artifact1_slot"),
|
||||||
|
.artifact2 = try store.getCodeIdJson(obj, "artifact2_slot"),
|
||||||
|
.artifact3 = try store.getCodeIdJson(obj, "artifact3_slot"),
|
||||||
|
.consumable1 = try Consumable.parse(store, obj, "consumable1_slot", "consumable1_slot_quantity"),
|
||||||
|
.consumable2 = try Consumable.parse(store, obj, "consumable2_slot", "consumable2_slot_quantity"),
|
||||||
|
};
|
||||||
|
}
|
||||||
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
72
api/schemas/item.zig
Normal file
72
api/schemas/item.zig
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Store = @import("../store.zig");
|
||||||
|
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||||
|
const json = std.json;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
|
||||||
|
const Craft = @import("./craft.zig");
|
||||||
|
|
||||||
|
const Item = @This();
|
||||||
|
|
||||||
|
pub const Type = enum {
|
||||||
|
consumable,
|
||||||
|
body_armor,
|
||||||
|
weapon,
|
||||||
|
resource,
|
||||||
|
leg_armor,
|
||||||
|
helmet,
|
||||||
|
boots,
|
||||||
|
shield,
|
||||||
|
amulet,
|
||||||
|
ring,
|
||||||
|
artifact,
|
||||||
|
currency,
|
||||||
|
};
|
||||||
|
pub const TypeUtils = EnumStringUtils(Type, .{
|
||||||
|
.{ "consumable", .consumable },
|
||||||
|
.{ "body_armor", .body_armor },
|
||||||
|
.{ "weapon" , .weapon },
|
||||||
|
.{ "resource" , .resource },
|
||||||
|
.{ "leg_armor" , .leg_armor },
|
||||||
|
.{ "helmet" , .helmet },
|
||||||
|
.{ "boots" , .boots },
|
||||||
|
.{ "shield" , .shield },
|
||||||
|
.{ "amulet" , .amulet },
|
||||||
|
.{ "ring" , .ring },
|
||||||
|
.{ "artifact" , .artifact },
|
||||||
|
.{ "currency" , .currency },
|
||||||
|
});
|
||||||
|
|
||||||
|
name: []u8,
|
||||||
|
code_id: Store.CodeId,
|
||||||
|
level: u64,
|
||||||
|
type: Type,
|
||||||
|
subtype: []u8,
|
||||||
|
description: []u8,
|
||||||
|
craft: ?Craft,
|
||||||
|
// TODO: effects
|
||||||
|
|
||||||
|
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 craft = json_utils.getObject(obj, "craft");
|
||||||
|
const item_type_str = try json_utils.getStringRequired(obj, "type");
|
||||||
|
|
||||||
|
return Item{
|
||||||
|
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
|
||||||
|
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
|
||||||
|
.level = @intCast(level),
|
||||||
|
.type = TypeUtils.fromString(item_type_str) orelse return error.InvalidType,
|
||||||
|
.subtype = (try json_utils.dupeString(allocator, obj, "subtype")) orelse return error.MissingProperty,
|
||||||
|
.description = (try json_utils.dupeString(allocator, obj, "description")) orelse return error.MissingProperty,
|
||||||
|
.craft = if (craft != null) try Craft.parse(store, craft.?) else null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Item, allocator: Allocator) void {
|
||||||
|
allocator.free(self.name);
|
||||||
|
allocator.free(self.subtype);
|
||||||
|
allocator.free(self.description);
|
||||||
|
}
|
||||||
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
34
api/schemas/map.zig
Normal file
34
api/schemas/map.zig
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Store = @import("../store.zig");
|
||||||
|
const Position = @import("../position.zig");
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
const json = std.json;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Map = @This();
|
||||||
|
|
||||||
|
const MapContent = @import("./map_content.zig");
|
||||||
|
|
||||||
|
name: []u8,
|
||||||
|
skin: []u8,
|
||||||
|
position: Position,
|
||||||
|
content: ?MapContent,
|
||||||
|
|
||||||
|
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Map {
|
||||||
|
const content = json_utils.getObject(obj, "content");
|
||||||
|
|
||||||
|
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 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 = if (content) |c| try MapContent.parse(store, c) else null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
80
api/schemas/monster.zig
Normal file
80
api/schemas/monster.zig
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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 DropRate = @import("./drop_rate.zig");
|
||||||
|
const DropRates = DropRate.DropRates;
|
||||||
|
|
||||||
|
const Monster = @This();
|
||||||
|
|
||||||
|
pub const ElementalStats = struct {
|
||||||
|
attack: i64,
|
||||||
|
resistance: i64,
|
||||||
|
|
||||||
|
pub fn parse(object: json.ObjectMap, attack: []const u8, resistance: []const u8) !ElementalStats {
|
||||||
|
return ElementalStats{
|
||||||
|
.attack = try json_utils.getIntegerRequired(object, attack),
|
||||||
|
.resistance = try json_utils.getIntegerRequired(object, resistance),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
name: []u8,
|
||||||
|
code_id: Store.CodeId,
|
||||||
|
level: u64,
|
||||||
|
hp: u64,
|
||||||
|
min_gold: u64,
|
||||||
|
max_gold: u64,
|
||||||
|
|
||||||
|
fire: ElementalStats,
|
||||||
|
earth: ElementalStats,
|
||||||
|
water: ElementalStats,
|
||||||
|
air: ElementalStats,
|
||||||
|
|
||||||
|
drops: DropRates,
|
||||||
|
|
||||||
|
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 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 deinit(self: Monster, allocator: Allocator) void {
|
||||||
|
allocator.free(self.name);
|
||||||
|
}
|
||||||
51
api/schemas/resource.zig
Normal file
51
api/schemas/resource.zig
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Store = @import("../store.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();
|
||||||
|
|
||||||
|
pub const Skill = enum {
|
||||||
|
mining,
|
||||||
|
woodcutting,
|
||||||
|
fishing,
|
||||||
|
};
|
||||||
|
pub const SkillUtils = EnumStringUtils(Skill, .{
|
||||||
|
.{ "mining" , .mining },
|
||||||
|
.{ "woodcutting", .woodcutting },
|
||||||
|
.{ "fishing" , .fishing },
|
||||||
|
});
|
||||||
|
|
||||||
|
name: []u8,
|
||||||
|
code_id: Store.CodeId,
|
||||||
|
skill: Skill,
|
||||||
|
level: u64,
|
||||||
|
drops: DropRates,
|
||||||
|
|
||||||
|
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Resource {
|
||||||
|
const drops_array = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
|
||||||
|
|
||||||
|
const level = try json_utils.getIntegerRequired(obj, "level");
|
||||||
|
if (level < 0) {
|
||||||
|
return error.InvalidLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skill_str = try json_utils.getStringRequired(obj, "skill");
|
||||||
|
|
||||||
|
return Resource{
|
||||||
|
.name = try json_utils.dupeStringRequired(allocator, obj, "name"),
|
||||||
|
.code_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 deinit(self: Resource, allocator: Allocator) void {
|
||||||
|
allocator.free(self.name);
|
||||||
|
}
|
||||||
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,5 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const json_utils = @import("./json_utils.zig");
|
const json_utils = @import("../json_utils.zig");
|
||||||
const json = std.json;
|
const json = std.json;
|
||||||
|
|
||||||
const SkillStats = @This();
|
const SkillStats = @This();
|
||||||
@ -1,31 +1,31 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("../json_utils.zig");
|
||||||
const Server = @import("./server.zig");
|
const Store = @import("../store.zig");
|
||||||
const ItemId = Server.ItemId;
|
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const json = std.json;
|
const json = std.json;
|
||||||
|
|
||||||
const Slot = @import("./slot.zig");
|
const ItemQuantity = @import("./item_quantity.zig");
|
||||||
|
|
||||||
pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
||||||
const Slots = std.BoundedArray(Slot, slot_count);
|
const Slots = std.BoundedArray(ItemQuantity, slot_count);
|
||||||
|
const CodeId = Store.CodeId;
|
||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
slots: Slots,
|
slots: Slots,
|
||||||
|
|
||||||
fn init() @This() {
|
pub fn init() @This() {
|
||||||
return @This(){
|
return @This(){
|
||||||
.slots = Slots.init(0) catch unreachable
|
.slots = Slots.init(0) catch unreachable
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(api: *Server, slots_array: json.Array) !@This() {
|
pub fn parse(api: *Store, slots_array: json.Array) !@This() {
|
||||||
var slots = Slots.init(0) catch unreachable;
|
var slots = Slots.init(0) catch unreachable;
|
||||||
|
|
||||||
for (slots_array.items) |slot_value| {
|
for (slots_array.items) |slot_value| {
|
||||||
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
|
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
|
||||||
|
|
||||||
if (try Slot.parse(api, slot_obj)) |slot| {
|
if (try ItemQuantity.parse(api, slot_obj)) |slot| {
|
||||||
try slots.append(slot);
|
try slots.append(slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
|||||||
return @This(){ .slots = slots };
|
return @This(){ .slots = slots };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findSlotIndex(self: *const @This(), id: ItemId) ?usize {
|
fn findSlotIndex(self: *const @This(), id: CodeId) ?usize {
|
||||||
for (0.., self.slots.slice()) |i, *slot| {
|
for (0.., self.slots.slice()) |i, *slot| {
|
||||||
if (slot.id == id) {
|
if (slot.id == id) {
|
||||||
return i;
|
return i;
|
||||||
@ -43,7 +43,7 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findSlot(self: *@This(), id: ItemId) ?*Slot {
|
fn findSlot(self: *@This(), id: CodeId) ?*ItemQuantity {
|
||||||
if (self.findSlotIndex(id)) |index| {
|
if (self.findSlotIndex(id)) |index| {
|
||||||
return &self.slots.buffer[index];
|
return &self.slots.buffer[index];
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *@This(), id: ItemId, quantity: u64) void {
|
pub fn remove(self: *@This(), id: CodeId, quantity: u64) void {
|
||||||
const slot_index = self.findSlotIndex(id) orelse unreachable;
|
const slot_index = self.findSlotIndex(id) orelse unreachable;
|
||||||
const slot = self.slots.get(slot_index);
|
const slot = self.slots.get(slot_index);
|
||||||
assert(slot.quantity >= quantity);
|
assert(slot.quantity >= quantity);
|
||||||
@ -62,36 +62,29 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(self: *@This(), id: ItemId, quantity: u64) void {
|
pub fn add(self: *@This(), id: CodeId, quantity: u64) !void {
|
||||||
|
if (quantity == 0) return;
|
||||||
|
|
||||||
if (self.findSlot(id)) |slot| {
|
if (self.findSlot(id)) |slot| {
|
||||||
slot.quantity += quantity;
|
slot.quantity += quantity;
|
||||||
} else {
|
} else {
|
||||||
var empty_slot: ?*Slot = null;
|
try self.slots.append(ItemQuantity.init(id, quantity));
|
||||||
for (&self.slots) |*slot| {
|
|
||||||
if (slot.id == null) {
|
|
||||||
empty_slot = slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(empty_slot != null);
|
|
||||||
empty_slot.?.id = id;
|
|
||||||
empty_slot.?.quantity = quantity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addSlice(self: *@This(), items: []const Server.ItemIdQuantity) void {
|
pub fn addSlice(self: *@This(), items: []const ItemQuantity) void {
|
||||||
for (items) |item| {
|
for (items) |item| {
|
||||||
self.add(item.id, item.quantity);
|
self.add(item.id, item.quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removeSlice(self: *@This(), items: []const Server.ItemIdQuantity) void {
|
pub fn removeSlice(self: *@This(), items: []const ItemQuantity) void {
|
||||||
for (items) |item| {
|
for (items) |item| {
|
||||||
self.remove(item.id, item.quantity);
|
self.remove(item.id, item.quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getQuantity(self: *const @This(), id: ItemId) u64 {
|
pub fn getQuantity(self: *const @This(), id: CodeId) u64 {
|
||||||
if (self.findSlotIndex(id)) |index| {
|
if (self.findSlotIndex(id)) |index| {
|
||||||
return self.slots.get(index).quantity;
|
return self.slots.get(index).quantity;
|
||||||
}
|
}
|
||||||
@ -107,7 +100,7 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slice(self: *@This()) []Slot {
|
pub fn slice(self: *@This()) []ItemQuantity {
|
||||||
return self.slots.slice();
|
return self.slots.slice();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
32
api/schemas/status.zig
Normal file
32
api/schemas/status.zig
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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 ServerStatus = @This();
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
status: []const u8,
|
||||||
|
version: []const u8,
|
||||||
|
characters_online: u64,
|
||||||
|
|
||||||
|
pub fn parse(store: *Store, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
|
||||||
|
_ = store;
|
||||||
|
const characters_online = json_utils.getInteger(object, "characters_online") orelse return error.MissingProperty;
|
||||||
|
if (characters_online < 0) {
|
||||||
|
return error.InvalidCharactersOnline;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerStatus{
|
||||||
|
.allocator = allocator,
|
||||||
|
.characters_online = @intCast(characters_online),
|
||||||
|
.status = (try json_utils.dupeString(allocator, object, "status")) orelse return error.MissingStatus,
|
||||||
|
.version = (try json_utils.dupeString(allocator, object, "version")) orelse return error.MissingVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: ServerStatus) void {
|
||||||
|
self.allocator.free(self.status);
|
||||||
|
self.allocator.free(self.version);
|
||||||
|
}
|
||||||
36
api/schemas/task.zig
Normal file
36
api/schemas/task.zig
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Store = @import("../store.zig");
|
||||||
|
const json_utils = @import("../json_utils.zig");
|
||||||
|
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const Task = @This();
|
||||||
|
|
||||||
|
pub const Type = enum {
|
||||||
|
monsters,
|
||||||
|
resources,
|
||||||
|
crafts
|
||||||
|
};
|
||||||
|
pub const TypeUtils = EnumStringUtils(Type, .{
|
||||||
|
.{ "monsters" , Type.monsters },
|
||||||
|
.{ "resources", Type.resources },
|
||||||
|
.{ "crafts" , Type.crafts },
|
||||||
|
});
|
||||||
|
|
||||||
|
code_id: Store.CodeId,
|
||||||
|
type: Type,
|
||||||
|
total: u64,
|
||||||
|
|
||||||
|
pub fn parse(store: *Store, obj: json.ObjectMap) !Task {
|
||||||
|
const task_type = try json_utils.getStringRequired(obj, "type");
|
||||||
|
const total = try json_utils.getIntegerRequired(obj, "total");
|
||||||
|
if (total < 0) {
|
||||||
|
return error.InvalidTaskTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task{
|
||||||
|
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty,
|
||||||
|
.type = TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
|
||||||
|
.total = @intCast(total)
|
||||||
|
};
|
||||||
|
}
|
||||||
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
|
||||||
|
};
|
||||||
|
}
|
||||||
1082
api/server.zig
Normal file
1082
api/server.zig
Normal file
File diff suppressed because it is too large
Load Diff
422
api/store.zig
Normal file
422
api/store.zig
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json_utils = @import("json_utils.zig");
|
||||||
|
const Server = @import("./server.zig");
|
||||||
|
const s2s = @import("s2s");
|
||||||
|
const json = std.json;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Store = @This();
|
||||||
|
|
||||||
|
const Character = @import("./schemas/character.zig");
|
||||||
|
const Item = @import("./schemas/item.zig");
|
||||||
|
const Position = @import("./position.zig");
|
||||||
|
const Map = @import("./schemas/map.zig");
|
||||||
|
const Resource = @import("./schemas/resource.zig");
|
||||||
|
const Monster = @import("./schemas/monster.zig");
|
||||||
|
const DropRate = @import("./schemas/drop_rate.zig");
|
||||||
|
|
||||||
|
pub const CodeId = u16;
|
||||||
|
|
||||||
|
const Characters = std.ArrayList(Character);
|
||||||
|
const ItemsMap = std.StringHashMap(Item);
|
||||||
|
const MapsMap = std.AutoHashMap(Position, Map);
|
||||||
|
const ResourcesMap = std.StringHashMap(Resource);
|
||||||
|
const MonstersMap = std.StringHashMap(Monster);
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
codes: std.ArrayList([]u8),
|
||||||
|
characters: Characters,
|
||||||
|
items: ItemsMap,
|
||||||
|
maps: MapsMap,
|
||||||
|
resources: ResourcesMap,
|
||||||
|
monsters: MonstersMap,
|
||||||
|
// TODO: bank
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) Store {
|
||||||
|
return Store{
|
||||||
|
.allocator = allocator,
|
||||||
|
.codes = std.ArrayList([]u8).init(allocator),
|
||||||
|
.characters = Characters.init(allocator),
|
||||||
|
.items = ItemsMap.init(allocator),
|
||||||
|
.maps = MapsMap.init(allocator),
|
||||||
|
.resources = ResourcesMap.init(allocator),
|
||||||
|
.monsters = MonstersMap.init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Store) void {
|
||||||
|
self.clearCodes();
|
||||||
|
|
||||||
|
for (self.characters.items) |*char| {
|
||||||
|
char.deinit();
|
||||||
|
}
|
||||||
|
self.characters.deinit();
|
||||||
|
|
||||||
|
self.clearItems();
|
||||||
|
|
||||||
|
self.clearMaps();
|
||||||
|
|
||||||
|
self.clearResources();
|
||||||
|
|
||||||
|
self.clearMonsters();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCodeId(self: *Store, code: []const u8) !CodeId {
|
||||||
|
assert(code.len != 0);
|
||||||
|
|
||||||
|
for (0.., self.codes.items) |i, item_code| {
|
||||||
|
if (std.mem.eql(u8, code, item_code)) {
|
||||||
|
return @intCast(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const code_dupe = try self.allocator.dupe(u8, code);
|
||||||
|
errdefer self.allocator.free(code_dupe);
|
||||||
|
try self.codes.append(code_dupe);
|
||||||
|
|
||||||
|
return @intCast(self.codes.items.len - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCode(self: *const Store, id: CodeId) ?[]const u8 {
|
||||||
|
if (id >= self.codes.items.len) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.codes.items[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCodeIdJson(self: *Store, object: json.ObjectMap, name: []const u8) !?CodeId {
|
||||||
|
const code = try json_utils.getStringRequired(object, name);
|
||||||
|
if (code.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return try self.getCodeId(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clearCodes(self: *Store) void {
|
||||||
|
for (self.codes.items) |code| {
|
||||||
|
self.allocator.free(code);
|
||||||
|
}
|
||||||
|
self.codes.clearAndFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Storing to file ------------------------------
|
||||||
|
|
||||||
|
const SaveData = struct {
|
||||||
|
api_version: []const u8,
|
||||||
|
|
||||||
|
codes: [][]u8,
|
||||||
|
items: []Item,
|
||||||
|
maps: []Map,
|
||||||
|
resources: []Resource,
|
||||||
|
monsters: []Monster,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn allocHashMapValues(Value: type, allocator: Allocator, hashmap: anytype) ![]Value {
|
||||||
|
var values = try allocator.alloc(Value, hashmap.count());
|
||||||
|
errdefer allocator.free(values);
|
||||||
|
|
||||||
|
var valueIter = hashmap.valueIterator();
|
||||||
|
var index: usize = 0;
|
||||||
|
while (valueIter.next()) |value| {
|
||||||
|
values[index] = value.*;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(self: *Store, api_version: []const u8, writer: anytype) !void {
|
||||||
|
const items = try allocHashMapValues(Item, self.allocator, self.items);
|
||||||
|
defer self.allocator.free(items);
|
||||||
|
|
||||||
|
const maps = try allocHashMapValues(Map, self.allocator, self.maps);
|
||||||
|
defer self.allocator.free(maps);
|
||||||
|
|
||||||
|
const resources = try allocHashMapValues(Resource, self.allocator, self.resources);
|
||||||
|
defer self.allocator.free(resources);
|
||||||
|
|
||||||
|
const monsters = try allocHashMapValues(Monster, self.allocator, self.monsters);
|
||||||
|
defer self.allocator.free(monsters);
|
||||||
|
|
||||||
|
const data = SaveData{
|
||||||
|
.api_version = api_version,
|
||||||
|
.codes = self.codes.items,
|
||||||
|
.items = items,
|
||||||
|
.maps = maps,
|
||||||
|
.resources = resources,
|
||||||
|
.monsters = monsters
|
||||||
|
};
|
||||||
|
|
||||||
|
try s2s.serialize(writer, SaveData, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(self: *Store, api_version: []const u8, reader: anytype) !void {
|
||||||
|
var data = try s2s.deserializeAlloc(reader, SaveData, self.allocator);
|
||||||
|
if (!std.mem.eql(u8, data.api_version, api_version)) {
|
||||||
|
s2s.free(self.allocator, SaveData, &data);
|
||||||
|
return error.InvalidVersion;
|
||||||
|
}
|
||||||
|
defer self.allocator.free(data.api_version);
|
||||||
|
|
||||||
|
self.clearCodes();
|
||||||
|
try self.codes.appendSlice(data.codes);
|
||||||
|
defer self.allocator.free(data.codes);
|
||||||
|
|
||||||
|
self.clearItems();
|
||||||
|
for (data.items) |item| {
|
||||||
|
try self.putItem(item);
|
||||||
|
}
|
||||||
|
defer self.allocator.free(data.items);
|
||||||
|
|
||||||
|
self.clearMaps();
|
||||||
|
for (data.maps) |map| {
|
||||||
|
try self.putMap(map);
|
||||||
|
}
|
||||||
|
defer self.allocator.free(data.maps);
|
||||||
|
|
||||||
|
self.clearResources();
|
||||||
|
for (data.resources) |resource| {
|
||||||
|
try self.putResource(resource);
|
||||||
|
}
|
||||||
|
defer self.allocator.free(data.resources);
|
||||||
|
|
||||||
|
self.clearMonsters();
|
||||||
|
for (data.monsters) |monster| {
|
||||||
|
try self.putMonster(monster);
|
||||||
|
}
|
||||||
|
defer self.allocator.free(data.monsters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Character ------------------------------
|
||||||
|
|
||||||
|
fn getCharacterIndex(self: *const Store, name: []const u8) ?usize {
|
||||||
|
for (0.., self.characters.items) |i, character| {
|
||||||
|
if (std.mem.eql(u8, character.name, name)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCharacter(self: *Store, name: []const u8) ?Character {
|
||||||
|
if (self.getCharacterIndex(name)) |index| {
|
||||||
|
return self.characters.items[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn putCharacter(self: *Store, character: Character) !void {
|
||||||
|
if (self.getCharacterIndex(character.name)) |index| {
|
||||||
|
self.characters.items[index].deinit();
|
||||||
|
self.characters.items[index] = character;
|
||||||
|
} else {
|
||||||
|
try self.characters.append(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Map ------------------------------
|
||||||
|
|
||||||
|
pub fn getMap(self: *Store, x: i64, y: i64) ?Map {
|
||||||
|
const pos = Position.init(x, y);
|
||||||
|
return self.maps.get(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getMaps(self: *Store, opts: Server.MapOptions) !std.ArrayList(Map) {
|
||||||
|
var found = std.ArrayList(Map).init(self.allocator);
|
||||||
|
errdefer found.deinit();
|
||||||
|
|
||||||
|
var mapIter = self.maps.valueIterator();
|
||||||
|
while (mapIter.next()) |map| {
|
||||||
|
if (opts.type) |content_type| {
|
||||||
|
if (map.content == null) continue;
|
||||||
|
if (map.content.?.type != content_type) continue;
|
||||||
|
}
|
||||||
|
if (opts.code) |content_code| {
|
||||||
|
if (map.content == null) continue;
|
||||||
|
|
||||||
|
const map_content_code = self.getCode(map.content.?.code_id).?;
|
||||||
|
if (!std.mem.eql(u8, map_content_code, content_code)) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try found.append(map.*);
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn putMap(self: *Store, map: Map) !void {
|
||||||
|
var entry = try self.maps.getOrPut(map.position);
|
||||||
|
if (entry.found_existing) {
|
||||||
|
entry.value_ptr.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
entry.value_ptr.* = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clearMaps(self: *Store) void {
|
||||||
|
var mapsIter = self.maps.valueIterator();
|
||||||
|
while (mapsIter.next()) |map| {
|
||||||
|
map.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
self.maps.clearAndFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Item ------------------------------
|
||||||
|
|
||||||
|
pub fn getItem(self: *Store, code: []const u8) ?Item {
|
||||||
|
return self.items.get(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getItems(self: *Store, opts: Server.ItemOptions) !std.ArrayList(Item) {
|
||||||
|
var found = std.ArrayList(Item).init(self.allocator);
|
||||||
|
errdefer found.deinit();
|
||||||
|
|
||||||
|
var itemIter = self.items.valueIterator();
|
||||||
|
while (itemIter.next()) |item| {
|
||||||
|
if (opts.craft_skill) |craft_skill| {
|
||||||
|
if (item.craft == null) continue;
|
||||||
|
if (item.craft.?.skill != craft_skill) continue;
|
||||||
|
}
|
||||||
|
if (opts.craft_material) |craft_material| {
|
||||||
|
if (item.craft == null) continue;
|
||||||
|
const recipe = item.craft.?;
|
||||||
|
|
||||||
|
const craft_material_id = try self.getCodeId(craft_material);
|
||||||
|
const material_quantity = recipe.items.getQuantity(craft_material_id);
|
||||||
|
if (material_quantity == 0) continue;
|
||||||
|
}
|
||||||
|
if (opts.min_level) |min_level| {
|
||||||
|
if (item.level < min_level) continue;
|
||||||
|
}
|
||||||
|
if (opts.max_level) |max_level| {
|
||||||
|
if (item.level > max_level) continue;
|
||||||
|
}
|
||||||
|
if (opts.type) |item_type| {
|
||||||
|
if (item.type != item_type) continue;
|
||||||
|
}
|
||||||
|
if (opts.name) |name| {
|
||||||
|
if (std.mem.indexOf(u8, item.name, name) == null) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try found.append(item.*);
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn putItem(self: *Store, item: Item) !void {
|
||||||
|
const code = self.getCode(item.code_id).?;
|
||||||
|
var entry = try self.items.getOrPut(code);
|
||||||
|
if (entry.found_existing) {
|
||||||
|
entry.value_ptr.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
entry.value_ptr.* = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clearItems(self: *Store) void {
|
||||||
|
var itemsIter = self.items.valueIterator();
|
||||||
|
while (itemsIter.next()) |item| {
|
||||||
|
item.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
self.items.clearAndFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Monster ------------------------------
|
||||||
|
|
||||||
|
pub fn getMonster(self: *Store, code: []const u8) ?Monster {
|
||||||
|
return self.monsters.get(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getMonsters(self: *Store, opts: Server.MonsterOptions) !std.ArrayList(Monster) {
|
||||||
|
var found = std.ArrayList(Monster).init(self.allocator);
|
||||||
|
errdefer found.deinit();
|
||||||
|
|
||||||
|
var monsterIter = self.monsters.valueIterator();
|
||||||
|
while (monsterIter.next()) |monster| {
|
||||||
|
if (opts.min_level) |min_level| {
|
||||||
|
if (monster.level < min_level) continue;
|
||||||
|
}
|
||||||
|
if (opts.max_level) |max_level| {
|
||||||
|
if (monster.level > max_level) continue;
|
||||||
|
}
|
||||||
|
if (opts.drop) |drop| {
|
||||||
|
const item_id = try self.getCodeId(drop);
|
||||||
|
if (!DropRate.doesListContain(&monster.drops, item_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try found.append(monster.*);
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn putMonster(self: *Store, monster: Monster) !void {
|
||||||
|
const code = self.getCode(monster.code_id).?;
|
||||||
|
var entry = try self.monsters.getOrPut(code);
|
||||||
|
if (entry.found_existing) {
|
||||||
|
entry.value_ptr.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
entry.value_ptr.* = monster;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clearMonsters(self: *Store) void {
|
||||||
|
var monstersIter = self.monsters.valueIterator();
|
||||||
|
while (monstersIter.next()) |monster| {
|
||||||
|
monster.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
self.monsters.clearAndFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Resource ------------------------------
|
||||||
|
|
||||||
|
pub fn getResource(self: *Store, code: []const u8) ?Resource {
|
||||||
|
return self.resources.get(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getResources(self: *Store, opts: Server.ResourceOptions) !std.ArrayList(Resource) {
|
||||||
|
var found = std.ArrayList(Resource).init(self.allocator);
|
||||||
|
errdefer found.deinit();
|
||||||
|
|
||||||
|
var resourceIter = self.resources.valueIterator();
|
||||||
|
while (resourceIter.next()) |resource| {
|
||||||
|
if (opts.min_level) |min_level| {
|
||||||
|
if (resource.level < min_level) continue;
|
||||||
|
}
|
||||||
|
if (opts.max_level) |max_level| {
|
||||||
|
if (resource.level > max_level) continue;
|
||||||
|
}
|
||||||
|
if (opts.drop) |drop| {
|
||||||
|
const item_id = try self.getCodeId(drop);
|
||||||
|
if (!DropRate.doesListContain(&resource.drops, item_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try found.append(resource.*);
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn putResource(self: *Store, resource: Resource) !void {
|
||||||
|
const code = self.getCode(resource.code_id).?;
|
||||||
|
var entry = try self.resources.getOrPut(code);
|
||||||
|
if (entry.found_existing) {
|
||||||
|
entry.value_ptr.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
entry.value_ptr.* = resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clearResources(self: *Store) void {
|
||||||
|
var resourcesIter = self.resources.valueIterator();
|
||||||
|
while (resourcesIter.next()) |resource| {
|
||||||
|
resource.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
self.resources.clearAndFree();
|
||||||
|
}
|
||||||
115
build.zig
115
build.zig
@ -1,41 +1,98 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const ResolvedTarget = std.Build.ResolvedTarget;
|
||||||
|
const OptimizeMode = std.Build.OptimizeMode;
|
||||||
|
const Module = std.Build.Module;
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
var api: *Module = undefined;
|
||||||
.name = "artificer",
|
{
|
||||||
.root_source_file = b.path("src/main.zig"),
|
const s2s_dep = b.dependency("s2s", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
exe.linkLibC();
|
|
||||||
exe.addIncludePath(b.path("src/date_time"));
|
|
||||||
exe.addCSourceFile(.{ .file = b.path("src/date_time/timegm.c") });
|
|
||||||
|
|
||||||
b.installArtifact(exe);
|
api = b.createModule(.{
|
||||||
|
.root_source_file = b.path("api/root.zig"),
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
.target = target,
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
.optimize = optimize,
|
||||||
|
.link_libc = true,
|
||||||
if (b.args) |args| {
|
});
|
||||||
run_cmd.addArgs(args);
|
api.addIncludePath(b.path("api/date_time"));
|
||||||
|
api.addCSourceFile(.{ .file = b.path("api/date_time/timegm.c") });
|
||||||
|
api.addImport("s2s", s2s_dep.module("s2s"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const run_step = b.step("run", "Run the app");
|
var lib: *Module = undefined;
|
||||||
run_step.dependOn(&run_cmd.step);
|
{
|
||||||
|
lib = b.createModule(.{
|
||||||
|
.root_source_file = b.path("lib/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
lib.addImport("artifacts-api", api);
|
||||||
|
|
||||||
const exe_unit_tests = b.addTest(.{
|
const unit_tests = b.addTest(.{
|
||||||
.root_source_file = b.path("src/artifacts.zig"),
|
.root_source_file = b.path("lib/root.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
exe_unit_tests.linkLibC();
|
|
||||||
exe_unit_tests.addIncludePath(b.path("src"));
|
|
||||||
exe_unit_tests.addCSourceFile(.{ .file = b.path("src/timegm.c") });
|
|
||||||
|
|
||||||
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||||
const test_step = b.step("test", "Run unit tests");
|
const test_step = b.step("test-lib", "Run lib unit tests");
|
||||||
test_step.dependOn(&run_exe_unit_tests.step);
|
test_step.dependOn(&run_unit_tests.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const cli = b.addExecutable(.{
|
||||||
|
.name = "artificer",
|
||||||
|
.root_source_file = b.path("cli/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
cli.root_module.addImport("artificer", lib);
|
||||||
|
cli.root_module.addImport("artifacts-api", api);
|
||||||
|
b.installArtifact(cli);
|
||||||
|
|
||||||
|
const run_cmd = b.addRunArtifact(cli);
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const run_step = b.step("run-cli", "Run the CLI");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const raylib_dep = b.dependency("raylib-zig", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gui = b.addExecutable(.{
|
||||||
|
.name = "artificer-gui",
|
||||||
|
.root_source_file = b.path("gui/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
gui.root_module.addImport("artificer", lib);
|
||||||
|
gui.linkLibrary(raylib_dep.artifact("raylib"));
|
||||||
|
gui.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
||||||
|
|
||||||
|
b.installArtifact(gui);
|
||||||
|
|
||||||
|
const run_cmd = b.addRunArtifact(gui);
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const run_step = b.step("run-gui", "Run the GUI");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,15 @@
|
|||||||
.name = "artificer",
|
.name = "artificer",
|
||||||
.version = "0.1.0",
|
.version = "0.1.0",
|
||||||
.minimum_zig_version = "0.12.0",
|
.minimum_zig_version = "0.12.0",
|
||||||
.dependencies = .{ },
|
.dependencies = .{
|
||||||
.paths = .{ "" },
|
.@"raylib-zig" = .{
|
||||||
|
.url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz",
|
||||||
|
.hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212"
|
||||||
|
},
|
||||||
|
.s2s = .{
|
||||||
|
.url = "https://github.com/ziglibs/s2s/archive/b30205d5e9204899fb6d0fdf28d00ed4d18fe9c9.tar.gz",
|
||||||
|
.hash = "12202c39c98f05041f1052c268132669dbfcda87e4dbb0353cd84a6070924c8ac0e3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
53
cli/main.zig
Normal file
53
cli/main.zig
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Artificer = @import("artificer");
|
||||||
|
const Api = @import("artifacts-api");
|
||||||
|
|
||||||
|
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
if (args.len < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = args[1];
|
||||||
|
const cwd = std.fs.cwd();
|
||||||
|
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 "));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
|
||||||
|
defer allocator.free(token);
|
||||||
|
|
||||||
|
var artificer = try Artificer.init(allocator, token);
|
||||||
|
defer artificer.deinit();
|
||||||
|
|
||||||
|
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
|
||||||
|
defer allocator.free(cache_path);
|
||||||
|
|
||||||
|
std.log.info("Prefetching server data", .{});
|
||||||
|
try artificer.server.prefetchCached(cache_path);
|
||||||
|
|
||||||
|
if (false) {
|
||||||
|
std.log.info("Starting main loop", .{});
|
||||||
|
while (true) {
|
||||||
|
const waitUntil = artificer.nextStepAt();
|
||||||
|
const duration = waitUntil - std.time.milliTimestamp();
|
||||||
|
if (duration > 0) {
|
||||||
|
std.time.sleep(@intCast(duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
try artificer.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
gui/main.zig
Normal file
104
gui/main.zig
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Artificer = @import("artificer");
|
||||||
|
const rl = @import("raylib");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const srcery = @import("./srcery.zig");
|
||||||
|
|
||||||
|
const UI = @import("./ui.zig");
|
||||||
|
const UIStack = @import("./ui_stack.zig");
|
||||||
|
const RectUtils = @import("./rect_utils.zig");
|
||||||
|
|
||||||
|
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
if (args.len < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = args[1];
|
||||||
|
const cwd = std.fs.cwd();
|
||||||
|
var token_buffer: [256]u8 = undefined;
|
||||||
|
const token = try cwd.readFile(filename, &token_buffer);
|
||||||
|
|
||||||
|
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void {
|
||||||
|
var buffer: [256]u8 = undefined;
|
||||||
|
|
||||||
|
const name_height = 20;
|
||||||
|
UI.drawTextCentered(ui.font, brain.name, .{
|
||||||
|
.x = RectUtils.center(rect).x,
|
||||||
|
.y = rect.y + name_height/2
|
||||||
|
}, 20, 2, srcery.bright_white);
|
||||||
|
|
||||||
|
var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom);
|
||||||
|
label_stack.gap = 4;
|
||||||
|
|
||||||
|
const now = std.time.milliTimestamp();
|
||||||
|
const cooldown = brain.cooldown(&artificer.server);
|
||||||
|
const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s;
|
||||||
|
const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{ seconds_left });
|
||||||
|
UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
|
||||||
|
|
||||||
|
var task_label: []u8 = undefined;
|
||||||
|
if (brain.task) |task| {
|
||||||
|
task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{ @tagName(task) });
|
||||||
|
} else {
|
||||||
|
task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{ });
|
||||||
|
}
|
||||||
|
UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
|
||||||
|
|
||||||
|
const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{ brain.action_queue.items.len });
|
||||||
|
UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() anyerror!void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
|
||||||
|
defer allocator.free(token);
|
||||||
|
|
||||||
|
var artificer = try Artificer.init(allocator, token);
|
||||||
|
defer artificer.deinit();
|
||||||
|
|
||||||
|
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
|
||||||
|
defer allocator.free(cache_path);
|
||||||
|
|
||||||
|
std.log.info("Prefetching server data", .{});
|
||||||
|
try artificer.server.prefetchCached(cache_path);
|
||||||
|
|
||||||
|
rl.initWindow(800, 450, "Artificer");
|
||||||
|
defer rl.closeWindow();
|
||||||
|
|
||||||
|
rl.setTargetFPS(60);
|
||||||
|
|
||||||
|
var ui = UI.init();
|
||||||
|
defer ui.deinit();
|
||||||
|
|
||||||
|
while (!rl.windowShouldClose()) {
|
||||||
|
if (std.time.milliTimestamp() > artificer.nextStepAt()) {
|
||||||
|
try artificer.step();
|
||||||
|
}
|
||||||
|
|
||||||
|
const screen_size = rl.Vector2.init(
|
||||||
|
@floatFromInt(rl.getScreenWidth()),
|
||||||
|
@floatFromInt(rl.getScreenHeight())
|
||||||
|
);
|
||||||
|
|
||||||
|
rl.beginDrawing();
|
||||||
|
defer rl.endDrawing();
|
||||||
|
|
||||||
|
rl.clearBackground(srcery.black);
|
||||||
|
|
||||||
|
var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
|
||||||
|
for (artificer.characters.items) |*brain| {
|
||||||
|
const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len));
|
||||||
|
try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
gui/rect_utils.zig
Normal file
142
gui/rect_utils.zig
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
const rl = @import("raylib");
|
||||||
|
const Rect = rl.Rectangle;
|
||||||
|
|
||||||
|
pub const AlignX = enum { left, center, right };
|
||||||
|
pub const AlignY = enum { top, center, bottom };
|
||||||
|
|
||||||
|
pub fn initCentered(rect: Rect, width: f32, height: f32) Rect {
|
||||||
|
const unused_width = rect.width - width;
|
||||||
|
const unused_height = rect.height - height;
|
||||||
|
return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center(rect: Rect) rl.Vector2 {
|
||||||
|
return rl.Vector2{
|
||||||
|
.x = rect.x + rect.width / 2,
|
||||||
|
.y = rect.y + rect.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottomLeft(rect: Rect) rl.Vector2 {
|
||||||
|
return rl.Vector2{
|
||||||
|
.x = rect.x,
|
||||||
|
.y = rect.y + rect.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottomRight(rect: Rect) rl.Vector2 {
|
||||||
|
return rl.Vector2{
|
||||||
|
.x = rect.x + rect.width,
|
||||||
|
.y = rect.y + rect.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn topLeft(rect: Rect) rl.Vector2 {
|
||||||
|
return rl.Vector2{
|
||||||
|
.x = rect.x,
|
||||||
|
.y = rect.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn topRight(rect: Rect) rl.Vector2 {
|
||||||
|
return rl.Vector2{
|
||||||
|
.x = rect.x + rect.width,
|
||||||
|
.y = rect.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 {
|
||||||
|
const x = switch(align_x) {
|
||||||
|
.left => rect.x,
|
||||||
|
.center => rect.x + rect.width/2,
|
||||||
|
.right => rect.x + rect.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
const y = switch(align_y) {
|
||||||
|
.top => rect.y,
|
||||||
|
.center => rect.y + rect.height/2,
|
||||||
|
.bottom => rect.y + rect.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
return rl.Vector2.init(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shrink(rect: Rect, x: f32, y: f32) rl.Rectangle {
|
||||||
|
return Rect.init(rect.x + x, rect.y + y, rect.width - 2 * x, rect.height - 2 * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shrinkX(rect: rl.Rectangle, offset: f32) rl.Rectangle {
|
||||||
|
return shrink(rect, offset, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shrinkY(rect: rl.Rectangle, offset: f32) rl.Rectangle {
|
||||||
|
return shrink(rect, 0, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shrinkTop(rect: rl.Rectangle, offset: f32) rl.Rectangle {
|
||||||
|
return Rect.init(rect.x, rect.y + offset, rect.width, rect.height - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grow(rect: Rect, x: f32, y: f32) rl.Rectangle {
|
||||||
|
return shrink(rect, -x, -y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn growY(rect: Rect, offset: f32) rl.Rectangle {
|
||||||
|
return grow(rect, 0, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position(rect: rl.Rectangle) rl.Vector2 {
|
||||||
|
return rl.Vector2.init(rect.x, rect.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInside(rect: rl.Rectangle, x: f32, y: f32) bool {
|
||||||
|
return (rect.x <= x and x < rect.x + rect.width) and (rect.y < y and y < rect.y + rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInsideVec2(rect: rl.Rectangle, vec2: rl.Vector2) bool {
|
||||||
|
return isInside(rect, vec2.x, vec2.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn top(rect: rl.Rectangle) f32 {
|
||||||
|
return rect.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottom(rect: rl.Rectangle) f32 {
|
||||||
|
return rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left(rect: rl.Rectangle) f32 {
|
||||||
|
return rect.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(rect: rl.Rectangle) f32 {
|
||||||
|
return rect.x + rect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verticalSplit(rect: rl.Rectangle, left_side_width: f32) [2]rl.Rectangle {
|
||||||
|
var left_side = rect;
|
||||||
|
left_side.width = left_side_width;
|
||||||
|
|
||||||
|
var right_side = rect;
|
||||||
|
right_side.x += left_side_width;
|
||||||
|
right_side.width -= left_side_width;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
left_side,
|
||||||
|
right_side
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn horizontalSplit(rect: rl.Rectangle, top_side_height: f32) [2]rl.Rectangle {
|
||||||
|
var top_side = rect;
|
||||||
|
top_side.height = top_side_height;
|
||||||
|
|
||||||
|
var bottom_side = rect;
|
||||||
|
bottom_side.y += top_side_height;
|
||||||
|
bottom_side.height -= top_side_height;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
top_side,
|
||||||
|
bottom_side
|
||||||
|
};
|
||||||
|
}
|
||||||
43
gui/srcery.zig
Normal file
43
gui/srcery.zig
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
const rl = @import("raylib");
|
||||||
|
|
||||||
|
fn rgb(r: u8, g: u8, b: u8) rl.Color {
|
||||||
|
return rl.Color.init(r, g, b, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary
|
||||||
|
pub const black = rgb(28 , 27 , 25 );
|
||||||
|
pub const red = rgb(239, 47 , 39 );
|
||||||
|
pub const green = rgb(81 , 159, 80 );
|
||||||
|
pub const yellow = rgb(251, 184, 41 );
|
||||||
|
pub const blue = rgb(44 , 120, 191);
|
||||||
|
pub const magenta = rgb(224, 44 , 109);
|
||||||
|
pub const cyan = rgb(10 , 174, 179);
|
||||||
|
pub const white = rgb(186, 166, 127);
|
||||||
|
pub const bright_black = rgb(145, 129, 117);
|
||||||
|
pub const bright_red = rgb(247, 83 , 65 );
|
||||||
|
pub const bright_green = rgb(152, 188, 55 );
|
||||||
|
pub const bright_yellow = rgb(254, 208, 110);
|
||||||
|
pub const bright_blue = rgb(104, 168, 228);
|
||||||
|
pub const bright_magenta = rgb(255, 92 , 143);
|
||||||
|
pub const bright_cyan = rgb(43 , 228, 208);
|
||||||
|
pub const bright_white = rgb(252, 232, 195);
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
pub const orange = rgb(255, 95, 0);
|
||||||
|
pub const bright_orange = rgb(255, 135, 0);
|
||||||
|
pub const hard_black = rgb(18, 18, 18);
|
||||||
|
pub const teal = rgb(0, 128, 128);
|
||||||
|
|
||||||
|
// Grays
|
||||||
|
pub const xgray1 = rgb(38 , 38 , 38 );
|
||||||
|
pub const xgray2 = rgb(48 , 48 , 48 );
|
||||||
|
pub const xgray3 = rgb(58 , 58 , 58 );
|
||||||
|
pub const xgray4 = rgb(68 , 68 , 68 );
|
||||||
|
pub const xgray5 = rgb(78 , 78 , 78 );
|
||||||
|
pub const xgray6 = rgb(88 , 88 , 88 );
|
||||||
|
pub const xgray7 = rgb(98 , 98 , 98 );
|
||||||
|
pub const xgray8 = rgb(108, 108, 108);
|
||||||
|
pub const xgray9 = rgb(118, 118, 118);
|
||||||
|
pub const xgray10 = rgb(128, 128, 128);
|
||||||
|
pub const xgray11 = rgb(138, 138, 138);
|
||||||
|
pub const xgray12 = rgb(148, 148, 148);
|
||||||
159
gui/ui.zig
Normal file
159
gui/ui.zig
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
const rl = @import("raylib");
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const UI = @This();
|
||||||
|
|
||||||
|
font: rl.Font,
|
||||||
|
|
||||||
|
pub fn init() UI {
|
||||||
|
return UI{
|
||||||
|
.font = rl.getFontDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: UI) void {
|
||||||
|
rl.unloadFont(self.font);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reimplementation of `GetGlyphIndex` from raylib in src/rtext.c
|
||||||
|
fn GetGlyphIndex(font: rl.Font, codepoint: i32) usize {
|
||||||
|
var index: usize = 0;
|
||||||
|
|
||||||
|
var fallbackIndex: usize = 0; // Get index of fallback glyph '?'
|
||||||
|
|
||||||
|
for (0..@intCast(font.glyphCount), font.glyphs) |i, glyph| {
|
||||||
|
if (glyph.value == '?') fallbackIndex = i;
|
||||||
|
|
||||||
|
if (glyph.value == codepoint)
|
||||||
|
{
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((index == 0) and (font.glyphs[0].value != codepoint)) index = fallbackIndex;
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn GetCodePointNext(text: []const u8, next: *usize) i32 {
|
||||||
|
var letter: i32 = '?';
|
||||||
|
|
||||||
|
if (std.unicode.utf8ByteSequenceLength(text[0])) |codepointSize| {
|
||||||
|
next.* = codepointSize;
|
||||||
|
if (std.unicode.utf8Decode(text[0..codepointSize])) |codepoint| {
|
||||||
|
letter = @intCast(codepoint);
|
||||||
|
} else |_| {}
|
||||||
|
} else |_| {}
|
||||||
|
|
||||||
|
return letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Line spacing is a global variable, use SetTextLineSpacing() to setup
|
||||||
|
const textLineSpacing = 2; // TODO: Assume that line spacing is not changed.
|
||||||
|
|
||||||
|
// Reimplementation of `rl.drawTextEx`, so a null terminated would not be required
|
||||||
|
pub fn drawTextEx(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, tint: rl.Color) void {
|
||||||
|
var used_font = font;
|
||||||
|
if (font.texture.id == 0) {
|
||||||
|
used_font = rl.getFontDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
lib/root.zig
Normal file
136
lib/root.zig
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Api = @import("artifacts-api");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const Brain = @import("./brain.zig");
|
||||||
|
pub const TaskGraph = @import("./task_graph.zig");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,90 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const json_utils = @import("json_utils.zig");
|
|
||||||
const Server = @import("./server.zig");
|
|
||||||
const ItemId = Server.ItemId;
|
|
||||||
const json = std.json;
|
|
||||||
|
|
||||||
const Equipment = @This();
|
|
||||||
|
|
||||||
pub const Consumable = struct {
|
|
||||||
id: ?ItemId,
|
|
||||||
quantity: i64,
|
|
||||||
|
|
||||||
fn parse(api: *Server, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
|
|
||||||
return Consumable{
|
|
||||||
.id = try api.getItemIdJson(obj, name),
|
|
||||||
.quantity = try json_utils.getIntegerRequired(obj, quantity),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Slot = enum {
|
|
||||||
weapon,
|
|
||||||
shield,
|
|
||||||
helmet,
|
|
||||||
body_armor,
|
|
||||||
leg_armor,
|
|
||||||
boots,
|
|
||||||
ring1,
|
|
||||||
ring2,
|
|
||||||
amulet,
|
|
||||||
artifact1,
|
|
||||||
artifact2,
|
|
||||||
consumable1,
|
|
||||||
consumable2,
|
|
||||||
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
weapon: ?ItemId,
|
|
||||||
shield: ?ItemId,
|
|
||||||
helmet: ?ItemId,
|
|
||||||
body_armor: ?ItemId,
|
|
||||||
leg_armor: ?ItemId,
|
|
||||||
boots: ?ItemId,
|
|
||||||
|
|
||||||
ring1: ?ItemId,
|
|
||||||
ring2: ?ItemId,
|
|
||||||
amulet: ?ItemId,
|
|
||||||
|
|
||||||
artifact1: ?ItemId,
|
|
||||||
artifact2: ?ItemId,
|
|
||||||
artifact3: ?ItemId,
|
|
||||||
|
|
||||||
consumable1: Consumable,
|
|
||||||
consumable2: Consumable,
|
|
||||||
|
|
||||||
pub fn parse(api: *Server, obj: json.ObjectMap) !Equipment {
|
|
||||||
return Equipment{
|
|
||||||
.weapon = try api.getItemIdJson(obj, "weapon_slot"),
|
|
||||||
.shield = try api.getItemIdJson(obj, "shield_slot"),
|
|
||||||
.helmet = try api.getItemIdJson(obj, "helmet_slot"),
|
|
||||||
.body_armor = try api.getItemIdJson(obj, "body_armor_slot"),
|
|
||||||
.leg_armor = try api.getItemIdJson(obj, "leg_armor_slot"),
|
|
||||||
.boots = try api.getItemIdJson(obj, "boots_slot"),
|
|
||||||
.ring1 = try api.getItemIdJson(obj, "ring1_slot"),
|
|
||||||
.ring2 = try api.getItemIdJson(obj, "ring2_slot"),
|
|
||||||
.amulet = try api.getItemIdJson(obj, "amulet_slot"),
|
|
||||||
.artifact1 = try api.getItemIdJson(obj, "artifact1_slot"),
|
|
||||||
.artifact2 = try api.getItemIdJson(obj, "artifact2_slot"),
|
|
||||||
.artifact3 = try api.getItemIdJson(obj, "artifact3_slot"),
|
|
||||||
.consumable1 = try Consumable.parse(api, obj, "consumable1_slot", "consumable1_slot_quantity"),
|
|
||||||
.consumable2 = try Consumable.parse(api, obj, "consumable2_slot", "consumable2_slot_quantity"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
1466
src/api/server.zig
1466
src/api/server.zig
File diff suppressed because it is too large
Load Diff
@ -1,23 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const json_utils = @import("json_utils.zig");
|
|
||||||
const Server = @import("./server.zig");
|
|
||||||
const ItemId = Server.ItemId;
|
|
||||||
const json = std.json;
|
|
||||||
|
|
||||||
const ItemSlot = @This();
|
|
||||||
|
|
||||||
id: ItemId,
|
|
||||||
quantity: u64,
|
|
||||||
|
|
||||||
pub fn parse(api: *Server, slot_obj: json.ObjectMap) !?ItemSlot {
|
|
||||||
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 ItemSlot{
|
|
||||||
.id = try api.getItemId(code),
|
|
||||||
.quantity = @intCast(quantity),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,612 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Server = @import("./api/server.zig");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Position = Server.Position;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const CharacterBrain = @This();
|
|
||||||
|
|
||||||
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
|
|
||||||
|
|
||||||
pub const QueuedAction = union(enum) {
|
|
||||||
move: Position,
|
|
||||||
fight,
|
|
||||||
gather,
|
|
||||||
deposit_gold: u64,
|
|
||||||
deposit_item: Server.ItemIdQuantity,
|
|
||||||
withdraw_item: Server.ItemIdQuantity,
|
|
||||||
craft_item: Server.ItemIdQuantity,
|
|
||||||
|
|
||||||
pub fn perform(self: QueuedAction, api: *Server, name: []const u8, ) !QueuedActionResult {
|
|
||||||
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.getItemCode(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.getItemCode(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.getItemCode(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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const QueuedActionResult = union(enum) {
|
|
||||||
move: Server.MoveError!Server.MoveResult,
|
|
||||||
fight: Server.FightError!Server.FightResult,
|
|
||||||
gather: Server.GatherError!Server.GatherResult,
|
|
||||||
deposit_gold: Server.BankDepositGoldError!Server.GoldTransactionResult,
|
|
||||||
deposit_item: Server.BankDepositItemError!Server.ItemTransactionResult,
|
|
||||||
withdraw_item: Server.BankWithdrawItemError!Server.ItemTransactionResult,
|
|
||||||
craft_item: Server.CraftError!Server.CraftResult,
|
|
||||||
|
|
||||||
const Tag = @typeInfo(QueuedActionResult).Union.tag_type.?;
|
|
||||||
|
|
||||||
fn fieldType(comptime kind: Tag) type {
|
|
||||||
const field_type = std.meta.fields(QueuedActionResult)[@intFromEnum(kind)].type;
|
|
||||||
return @typeInfo(field_type).ErrorUnion.payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self: QueuedActionResult, comptime kind: Tag) ?fieldType(kind) {
|
|
||||||
return switch (self) {
|
|
||||||
kind => |v| v catch null,
|
|
||||||
else => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
comptime {
|
|
||||||
assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
name: []const u8,
|
|
||||||
routine: union (enum) {
|
|
||||||
idle,
|
|
||||||
|
|
||||||
fight: struct {
|
|
||||||
at: Position,
|
|
||||||
target: Server.ItemIdQuantity,
|
|
||||||
progress: u64 = 0,
|
|
||||||
},
|
|
||||||
gather: struct {
|
|
||||||
at: Position,
|
|
||||||
target: Server.ItemIdQuantity,
|
|
||||||
progress: u64 = 0,
|
|
||||||
},
|
|
||||||
craft: struct {
|
|
||||||
target: Server.ItemIdQuantity,
|
|
||||||
progress: u64 = 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
action_queue: std.ArrayList(QueuedAction),
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, name: []const u8) !CharacterBrain {
|
|
||||||
return CharacterBrain{
|
|
||||||
.name = try allocator.dupe(u8, name),
|
|
||||||
.routine = .idle,
|
|
||||||
.action_queue = std.ArrayList(QueuedAction).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: CharacterBrain) void {
|
|
||||||
const allocator = self.action_queue.allocator;
|
|
||||||
allocator.free(self.name);
|
|
||||||
self.action_queue.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn currentTime() f64 {
|
|
||||||
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
|
||||||
return timestamp / std.time.ms_per_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn performNextAction(self: *CharacterBrain, api: *Server) !void {
|
|
||||||
const log = std.log.default;
|
|
||||||
assert(self.action_queue.items.len > 0);
|
|
||||||
|
|
||||||
const APIError = Server.APIError;
|
|
||||||
|
|
||||||
const retry_delay = 0.5; // 500ms
|
|
||||||
var character = api.findCharacterPtr(self.name).?;
|
|
||||||
|
|
||||||
const next_action = self.action_queue.items[0];
|
|
||||||
const action_result = try next_action.perform(api, self.name);
|
|
||||||
switch (action_result) {
|
|
||||||
.fight => |result| {
|
|
||||||
const FightError = Server.FightError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
FightError.CharacterInCooldown,
|
|
||||||
FightError.CharacterIsBusy => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry fighting", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
FightError.CharacterIsFull,
|
|
||||||
FightError.MonsterNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
FightError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.move => |result| {
|
|
||||||
const MoveError = Server.MoveError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
MoveError.CharacterIsBusy,
|
|
||||||
MoveError.CharacterInCooldown => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry moving", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
MoveError.CharacterAtDestination => {
|
|
||||||
// Not great, but I guess the goal achieved? The character is at the desired location.
|
|
||||||
log.warn("[{s}] tried to move, but already at destination", .{self.name});
|
|
||||||
},
|
|
||||||
|
|
||||||
MoveError.MapNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
MoveError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.deposit_gold => |result| {
|
|
||||||
const BankDepositGoldError = Server.BankDepositGoldError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
BankDepositGoldError.BankIsBusy,
|
|
||||||
BankDepositGoldError.CharacterIsBusy,
|
|
||||||
BankDepositGoldError.CharacterInCooldown => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry depositing gold", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
BankDepositGoldError.NotEnoughGold,
|
|
||||||
BankDepositGoldError.BankNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
BankDepositGoldError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.deposit_item => |result| {
|
|
||||||
const BankDepositItemError = Server.BankDepositItemError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
BankDepositItemError.BankIsBusy,
|
|
||||||
BankDepositItemError.CharacterIsBusy,
|
|
||||||
BankDepositItemError.CharacterInCooldown => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry depositing item", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
BankDepositItemError.ItemNotFound,
|
|
||||||
BankDepositItemError.NotEnoughItems,
|
|
||||||
BankDepositItemError.BankNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
BankDepositItemError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.withdraw_item => |result| {
|
|
||||||
const BankWithdrawItemError = Server.BankWithdrawItemError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
BankWithdrawItemError.CharacterIsBusy,
|
|
||||||
BankWithdrawItemError.CharacterInCooldown,
|
|
||||||
BankWithdrawItemError.BankIsBusy => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry withdrawing item", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
BankWithdrawItemError.ItemNotFound,
|
|
||||||
BankWithdrawItemError.NotEnoughItems,
|
|
||||||
BankWithdrawItemError.CharacterIsFull,
|
|
||||||
BankWithdrawItemError.BankNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
BankWithdrawItemError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.gather => |result| {
|
|
||||||
const GatherError = Server.GatherError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
GatherError.CharacterInCooldown,
|
|
||||||
GatherError.CharacterIsBusy => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry withdrawing item", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
GatherError.NotEnoughSkill,
|
|
||||||
GatherError.CharacterIsFull,
|
|
||||||
GatherError.ResourceNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
GatherError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.craft_item => |result| {
|
|
||||||
const CraftError = Server.CraftError;
|
|
||||||
_ = result catch |err| switch (err) {
|
|
||||||
CraftError.CharacterInCooldown,
|
|
||||||
CraftError.CharacterIsBusy => {
|
|
||||||
// A bit too eager, retry action
|
|
||||||
character.cooldown_expiration = currentTime() + retry_delay;
|
|
||||||
log.warn("[{s}] retry withdrawing item", .{self.name});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
CraftError.RecipeNotFound,
|
|
||||||
CraftError.NotEnoughItems,
|
|
||||||
CraftError.NotEnoughSkill,
|
|
||||||
CraftError.CharacterIsFull,
|
|
||||||
CraftError.WorkshopNotFound => {
|
|
||||||
// Re-evaluate what the character should do, something is not right.
|
|
||||||
log.warn("[{s}] clear action queue", .{self.name});
|
|
||||||
self.action_queue.clearAndFree();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
CraftError.CharacterNotFound,
|
|
||||||
APIError.ServerUnavailable,
|
|
||||||
APIError.RequestFailed,
|
|
||||||
APIError.ParseFailed,
|
|
||||||
APIError.OutOfMemory => {
|
|
||||||
// Welp... Abondon ship. Bail. Bail
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = self.action_queue.orderedRemove(0);
|
|
||||||
|
|
||||||
self.onActionCompleted(action_result);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void {
|
|
||||||
switch (self.routine) {
|
|
||||||
.idle => {},
|
|
||||||
|
|
||||||
.fight => |*args| {
|
|
||||||
if (result.get(.fight)) |r| {
|
|
||||||
const fight_result: Server.FightResult = r;
|
|
||||||
const drops = fight_result.fight.drops;
|
|
||||||
|
|
||||||
args.progress += drops.getQuantity(args.target.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.gather => |*args| {
|
|
||||||
if (result.get(.gather)) |r| {
|
|
||||||
const gather_resutl: Server.GatherResult = r;
|
|
||||||
const items = gather_resutl.details.items;
|
|
||||||
|
|
||||||
args.progress += items.getQuantity(args.target.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.craft => |*args| {
|
|
||||||
if (result.get(.craft_item)) |r| {
|
|
||||||
const craft_result: Server.CraftResult = r;
|
|
||||||
const items = craft_result.details.items;
|
|
||||||
|
|
||||||
args.progress += items.getQuantity(args.target.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isRoutineFinished(self: *CharacterBrain) bool {
|
|
||||||
return switch (self.routine) {
|
|
||||||
.idle => false,
|
|
||||||
|
|
||||||
.fight => |args| {
|
|
||||||
return args.progress >= args.target.quantity;
|
|
||||||
},
|
|
||||||
.gather => |args| {
|
|
||||||
return args.progress >= args.target.quantity;
|
|
||||||
},
|
|
||||||
.craft => |args| {
|
|
||||||
return args.progress >= args.target.quantity;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn performRoutine(self: *CharacterBrain, api: *Server) !void {
|
|
||||||
switch (self.routine) {
|
|
||||||
.idle => {
|
|
||||||
std.log.debug("[{s}] idle", .{self.name});
|
|
||||||
},
|
|
||||||
.fight => |args| {
|
|
||||||
try self.fightRoutine(api, args.at);
|
|
||||||
},
|
|
||||||
.gather => |args| {
|
|
||||||
try self.gatherRoutine(api, args.at);
|
|
||||||
},
|
|
||||||
.craft => |args| {
|
|
||||||
try self.craftRoutine(api, args.target.id, args.target.quantity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn moveIfNeeded(self: *CharacterBrain, api: *Server, pos: Position) !bool {
|
|
||||||
const character = api.findCharacter(self.name).?;
|
|
||||||
|
|
||||||
if (character.position.eql(pos)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.action_queue.append(.{ .move = pos });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn depositItemsToBank(self: *CharacterBrain, api: *Server) !bool {
|
|
||||||
var character = api.findCharacter(self.name).?;
|
|
||||||
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: *CharacterBrain, api: *Server) !bool {
|
|
||||||
const character = api.findCharacter(self.name).?;
|
|
||||||
if (character.getItemCount() < character.inventory_max_items) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = try self.depositItemsToBank(api);
|
|
||||||
|
|
||||||
if (character.gold > 0) {
|
|
||||||
try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fightRoutine(self: *CharacterBrain, api: *Server, enemy_position: Position) !void {
|
|
||||||
if (try self.depositIfFull(api)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try self.moveIfNeeded(api, enemy_position)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.action_queue.append(.{ .fight = {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gatherRoutine(self: *CharacterBrain, api: *Server, resource_position: Position) !void {
|
|
||||||
if (try self.depositIfFull(api)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try self.moveIfNeeded(api, resource_position)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.action_queue.append(.{ .gather = {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn withdrawFromBank(self: *CharacterBrain, api: *Server, items: []const Server.Slot) !bool {
|
|
||||||
var character = api.findCharacter(self.name).?;
|
|
||||||
|
|
||||||
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(api, 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: *CharacterBrain, api: *Server, id: Server.ItemId, quantity: u64) !bool {
|
|
||||||
var character = api.findCharacter(self.name).?;
|
|
||||||
|
|
||||||
const inventory_quantity = character.inventory.getQuantity(id);
|
|
||||||
if (inventory_quantity >= quantity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = api.getItemCode(id) orelse return error.InvalidItemId;
|
|
||||||
const item = try api.getItem(code) orelse return error.ItemNotFound;
|
|
||||||
if (item.craft == null) {
|
|
||||||
return error.NotCraftable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipe = item.craft.?;
|
|
||||||
|
|
||||||
// TODO: Figure this out dynamically
|
|
||||||
const workstation = switch (recipe.skill) {
|
|
||||||
.weaponcrafting => Position{ .x = 2, .y = 1 },
|
|
||||||
.gearcrafting => Position{ .x = 3, .y = 1 },
|
|
||||||
.jewelrycrafting => Position{ .x = 1, .y = 3 },
|
|
||||||
.cooking => Position{ .x = 1, .y = 1 },
|
|
||||||
.woodcutting => Position{ .x = -2, .y = -3 },
|
|
||||||
.mining => Position{ .x = 1, .y = 5 },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (try self.moveIfNeeded(api, workstation)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.action_queue.append(.{ .craft_item = .{
|
|
||||||
.id = id,
|
|
||||||
.quantity = quantity - inventory_quantity
|
|
||||||
}});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn craftRoutine(self: *CharacterBrain, api: *Server, id: Server.ItemId, quantity: u64) !void {
|
|
||||||
var character = api.findCharacter(self.name).?;
|
|
||||||
const inventory_quantity = character.inventory.getQuantity(id);
|
|
||||||
if (inventory_quantity >= quantity) {
|
|
||||||
if (try self.depositItemsToBank(api)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = api.getItemCode(id) orelse return error.InvalidItemId;
|
|
||||||
const target_item = try api.getItem(code) orelse return error.ItemNotFound;
|
|
||||||
if (target_item.craft == null) {
|
|
||||||
return error.NotCraftable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipe = target_item.craft.?;
|
|
||||||
assert(recipe.quantity == 1); // TODO: Add support for recipe which produce multiple items
|
|
||||||
|
|
||||||
var needed_items = recipe.items;
|
|
||||||
for (needed_items.slice()) |*needed_item| {
|
|
||||||
needed_item.quantity *= quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try self.withdrawFromBank(api, needed_items.slice())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try self.craftItem(api, id, quantity)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
185
src/main.zig
185
src/main.zig
@ -1,185 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const Position = @import("./api/position.zig");
|
|
||||||
const Server = @import("./api/server.zig");
|
|
||||||
const CharacterBrain = @import("./character_brain.zig");
|
|
||||||
|
|
||||||
// pub const std_options = .{ .log_level = .debug };
|
|
||||||
|
|
||||||
fn currentTime() f64 {
|
|
||||||
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
|
||||||
return timestamp / std.time.ms_per_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GoalManager = struct {
|
|
||||||
api: *Server,
|
|
||||||
allocator: Allocator,
|
|
||||||
characters: std.ArrayList(CharacterBrain),
|
|
||||||
expiration_margin: f64 = 0.1, // 100ms
|
|
||||||
|
|
||||||
fn init(api: *Server, allocator: Allocator) GoalManager {
|
|
||||||
return GoalManager{
|
|
||||||
.api = api,
|
|
||||||
.allocator = allocator,
|
|
||||||
.characters = std.ArrayList(CharacterBrain).init(allocator)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn addCharacter(self: *GoalManager, name: []const u8) !void {
|
|
||||||
const character = try CharacterBrain.init(self.allocator, name);
|
|
||||||
try self.characters.append(character);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: GoalManager) void {
|
|
||||||
for (self.characters.items) |brain| {
|
|
||||||
brain.deinit();
|
|
||||||
}
|
|
||||||
self.characters.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runNextAction(self: *GoalManager) !void {
|
|
||||||
var earliest_cooldown: f64 = 0;
|
|
||||||
var earliest_character: ?*CharacterBrain = null;
|
|
||||||
for (self.characters.items) |*brain| {
|
|
||||||
if (brain.action_queue.items.len == 0) continue;
|
|
||||||
|
|
||||||
const character = self.api.findCharacter(brain.name).?;
|
|
||||||
if (earliest_character == null or earliest_cooldown > character.cooldown_expiration) {
|
|
||||||
earliest_character = brain;
|
|
||||||
earliest_cooldown = character.cooldown_expiration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (earliest_character == null) return;
|
|
||||||
|
|
||||||
const now = currentTime();
|
|
||||||
if (earliest_cooldown > now) {
|
|
||||||
const duration_s = earliest_cooldown - now + self.expiration_margin;
|
|
||||||
const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s));
|
|
||||||
std.log.debug("waiting for {d:.3}s", .{duration_s});
|
|
||||||
std.time.sleep(std.time.ns_per_ms * duration_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
try earliest_character.?.performNextAction(self.api);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
|
||||||
const args = try std.process.argsAlloc(allocator);
|
|
||||||
defer std.process.argsFree(allocator, args);
|
|
||||||
|
|
||||||
if (args.len < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = args[1];
|
|
||||||
const cwd = std.fs.cwd();
|
|
||||||
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 "));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() !void {
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
var api = try Server.init(allocator);
|
|
||||||
defer api.deinit();
|
|
||||||
|
|
||||||
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
|
|
||||||
defer allocator.free(token);
|
|
||||||
|
|
||||||
try api.setToken(token);
|
|
||||||
|
|
||||||
var goal_manager = GoalManager.init(&api, allocator);
|
|
||||||
defer goal_manager.deinit();
|
|
||||||
|
|
||||||
const chars = try api.listMyCharacters();
|
|
||||||
defer chars.deinit();
|
|
||||||
|
|
||||||
for (chars.items) |char| {
|
|
||||||
try goal_manager.addCharacter(char.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
goal_manager.characters.items[0].routine = .{
|
|
||||||
.fight = .{
|
|
||||||
.at = Position.init(0, 1),
|
|
||||||
.target = .{
|
|
||||||
.id = try api.getItemId("egg"),
|
|
||||||
.quantity = 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
goal_manager.characters.items[1].routine = .{
|
|
||||||
.gather = .{
|
|
||||||
.at = Position.init(-1, 0),
|
|
||||||
.target = .{
|
|
||||||
.id = try api.getItemId("ash_wood"),
|
|
||||||
.quantity = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
goal_manager.characters.items[2].routine = .{
|
|
||||||
.gather = .{
|
|
||||||
.at = Position.init(2, 0),
|
|
||||||
.target = .{
|
|
||||||
.id = try api.getItemId("copper_ore"),
|
|
||||||
.quantity = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
goal_manager.characters.items[3].routine = .{
|
|
||||||
.gather = .{
|
|
||||||
.at = Position.init(4, 2),
|
|
||||||
.target = .{
|
|
||||||
.id = try api.getItemId("gudgeon"),
|
|
||||||
.quantity = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
goal_manager.characters.items[4].routine = .{
|
|
||||||
.fight = .{
|
|
||||||
.at = Position.init(0, 1),
|
|
||||||
.target = .{
|
|
||||||
.id = try api.getItemId("raw_chicken"),
|
|
||||||
.quantity = 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const APIError = Server.APIError;
|
|
||||||
std.log.info("Starting main loop", .{});
|
|
||||||
while (true) {
|
|
||||||
goal_manager.runNextAction() catch |err| switch (err) {
|
|
||||||
APIError.ServerUnavailable => {
|
|
||||||
// If the server is down, wait for a moment and try again.
|
|
||||||
std.time.sleep(std.time.ns_per_min * 5);
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: Log all other error to a file or something. So it could be review later on.
|
|
||||||
else => return err
|
|
||||||
};
|
|
||||||
|
|
||||||
for (goal_manager.characters.items) |*brain| {
|
|
||||||
if (brain.action_queue.items.len > 0) continue;
|
|
||||||
|
|
||||||
if (brain.isRoutineFinished()) {
|
|
||||||
if (!try brain.depositItemsToBank(&api)) {
|
|
||||||
brain.routine = .idle;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try brain.performRoutine(&api);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user