add crafting goal
This commit is contained in:
parent
d6a141d098
commit
df56eceab6
10
api/root.zig
10
api/root.zig
@ -18,7 +18,15 @@ pub const Item = @import("./schemas/item.zig");
|
||||
pub const Status = @import("./schemas/status.zig");
|
||||
pub const Position = @import("./schemas/position.zig");
|
||||
pub const Map = @import("./schemas/map.zig");
|
||||
// pub const Character = @import("./schemas/character.zig");
|
||||
pub const Character = @import("./schemas/character.zig");
|
||||
pub const Equipment = @import("./schemas/equipment.zig");
|
||||
pub const Craft = @import("./schemas/craft.zig");
|
||||
pub const Resource = @import("./schemas/resource.zig");
|
||||
pub const MoveResult = @import("./schemas/move_result.zig");
|
||||
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
|
||||
pub const GatherResult = SkillUsageResult;
|
||||
pub const CraftResult = SkillUsageResult;
|
||||
|
||||
// pub const ServerStatus = @import("./schemas/status.zig");
|
||||
// pub const Map = @import("./schemas/map.zig");
|
||||
// pub const Position = @import("position.zig");
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
pub const Equipment = @import("./equipment.zig");
|
||||
const Task = @import("./task.zig");
|
||||
const SimpleItem = @import("./simple_item.zig");
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Character = @This();
|
||||
|
||||
@ -139,6 +141,8 @@ equipment: Equipment,
|
||||
task: ?TaskMasterTask,
|
||||
inventory_max_items: u64,
|
||||
inventory: Inventory,
|
||||
position: Position,
|
||||
cooldown_expiration: ?f64,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
|
||||
const name = try json_utils.getStringRequired(obj, "name");
|
||||
@ -171,6 +175,14 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
|
||||
.air = try ElementalStats.parse(obj, "attack_air", "dmg_air", "res_air"),
|
||||
});
|
||||
|
||||
const x = try json_utils.getIntegerRequired(obj, "x");
|
||||
const y = try json_utils.getIntegerRequired(obj, "y");
|
||||
|
||||
var cooldown_expiration: ?f64 = null;
|
||||
if (json_utils.getString(obj, "cooldown_expiration")) |date_time| {
|
||||
cooldown_expiration = parseDateTime(date_time) orelse return error.FailedToParseCooldownExpiration;
|
||||
}
|
||||
|
||||
return Character{
|
||||
.name = try Name.fromSlice(name),
|
||||
.account = try Account.fromSlice(account),
|
||||
@ -181,7 +193,9 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
|
||||
.equipment = try Equipment.parse(store, obj),
|
||||
.task = try TaskMasterTask.parse(store, obj),
|
||||
.inventory_max_items = inventory_max_items,
|
||||
.inventory = try Inventory.parse(store, inventory)
|
||||
.inventory = try Inventory.parse(store, inventory),
|
||||
.position = Position.init(x, y),
|
||||
.cooldown_expiration = cooldown_expiration
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
74
api/schemas/cooldown.zig
Normal file
74
api/schemas/cooldown.zig
Normal file
@ -0,0 +1,74 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
|
||||
const Cooldown = @This();
|
||||
|
||||
pub const Reason = enum {
|
||||
movement,
|
||||
fight,
|
||||
crafting,
|
||||
gathering,
|
||||
buy_ge,
|
||||
sell_ge,
|
||||
cancel_ge,
|
||||
delete_item,
|
||||
deposit,
|
||||
withdraw,
|
||||
deposit_gold,
|
||||
withdraw_gold,
|
||||
equip,
|
||||
unequip,
|
||||
task,
|
||||
christmas_exchange,
|
||||
recycling,
|
||||
rest,
|
||||
use,
|
||||
buy_bank_expansion,
|
||||
|
||||
const Utils = EnumStringUtils(Reason, .{
|
||||
.{ "movement" , Reason.movement },
|
||||
.{ "fight" , Reason.fight },
|
||||
.{ "crafting" , Reason.crafting },
|
||||
.{ "gathering" , Reason.gathering },
|
||||
.{ "buy_ge" , Reason.buy_ge },
|
||||
.{ "sell_ge" , Reason.sell_ge },
|
||||
.{ "cancel_ge" , Reason.cancel_ge },
|
||||
.{ "delete_item" , Reason.delete_item },
|
||||
.{ "deposit" , Reason.deposit },
|
||||
.{ "withdraw" , Reason.withdraw },
|
||||
.{ "deposit_gold" , Reason.deposit_gold },
|
||||
.{ "withdraw_gold" , Reason.withdraw_gold },
|
||||
.{ "equip" , Reason.equip },
|
||||
.{ "unequip" , Reason.unequip },
|
||||
.{ "task" , Reason.task },
|
||||
.{ "christmas_exchange", Reason.christmas_exchange },
|
||||
.{ "recycling" , Reason.recycling },
|
||||
.{ "rest" , Reason.rest },
|
||||
.{ "use" , Reason.use },
|
||||
.{ "buy_bank_expansion", Reason.buy_bank_expansion },
|
||||
});
|
||||
|
||||
pub const fromString = Utils.fromString;
|
||||
pub const toString = Utils.toString;
|
||||
};
|
||||
|
||||
started_at: f64,
|
||||
expiration: f64,
|
||||
reason: Reason,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Cooldown {
|
||||
_ = store;
|
||||
const started_at = try json_utils.getStringRequired(obj, "started_at");
|
||||
const expiration = try json_utils.getStringRequired(obj, "expiration");
|
||||
const reason = try json_utils.getStringRequired(obj, "reason");
|
||||
|
||||
return Cooldown{
|
||||
.started_at = parseDateTime(started_at) orelse return error.InvalidStartedAt,
|
||||
.expiration = parseDateTime(expiration) orelse return error.InvalidExpiration,
|
||||
.reason = Reason.fromString(reason) orelse return error.InvalidReason
|
||||
};
|
||||
}
|
||||
@ -3,11 +3,13 @@ const Store = @import("../store.zig");
|
||||
const std = @import("std");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const SimpleItem = @import("./simple_item.zig");
|
||||
const Character = @import("./character.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
|
||||
const Craft = @This();
|
||||
|
||||
pub const Items = SimpleItem.BoundedArray(8);
|
||||
pub const max_items = 8;
|
||||
pub const Items = SimpleItem.BoundedArray(max_items);
|
||||
|
||||
pub const Skill = enum {
|
||||
weaponcrafting,
|
||||
@ -30,6 +32,18 @@ pub const Skill = enum {
|
||||
|
||||
pub const toString = Utils.toString;
|
||||
pub const fromString = Utils.fromString;
|
||||
|
||||
pub fn toCharacterSkill(self: Skill) Character.Skill {
|
||||
return switch (self) {
|
||||
.weaponcrafting => Character.Skill.weaponcrafting,
|
||||
.gearcrafting => Character.Skill.gearcrafting,
|
||||
.jewelrycrafting => Character.Skill.jewelrycrafting,
|
||||
.cooking => Character.Skill.cooking,
|
||||
.woodcutting => Character.Skill.woodcutting,
|
||||
.mining => Character.Skill.mining,
|
||||
.alchemy => Character.Skill.alchemy,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
skill: Skill,
|
||||
|
||||
@ -106,3 +106,21 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Equipment {
|
||||
.utility2 = try UtilitySlot.parse(store, obj, "utility2_slot", "utility2_slot_quantity"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSlot(self: Equipment, slot: Slot) ?Store.Id {
|
||||
return switch (slot) {
|
||||
.weapon => self.weapon,
|
||||
.shield => self.shield,
|
||||
.helmet => self.helmet,
|
||||
.body_armor => self.body_armor,
|
||||
.leg_armor => self.leg_armor,
|
||||
.boots => self.boots,
|
||||
.ring1 => self.ring1,
|
||||
.ring2 => self.ring2,
|
||||
.amulet => self.amulet,
|
||||
.artifact1 => self.artifact1,
|
||||
.artifact2 => self.artifact2,
|
||||
.utility1 => if (self.utility1) |util| util.id else null,
|
||||
.utility2 => if (self.utility2) |util| util.id else null,
|
||||
};
|
||||
}
|
||||
|
||||
43
api/schemas/ge_order.zig
Normal file
43
api/schemas/ge_order.zig
Normal file
@ -0,0 +1,43 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Character = @import("./character.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
|
||||
const Store = @import("../store.zig");
|
||||
const Item = @import("./item.zig");
|
||||
|
||||
const GEOrder = @This();
|
||||
|
||||
pub const max_id_size = 32;
|
||||
pub const Id = std.BoundedArray(u8, max_id_size);
|
||||
pub const Account = Character.Account;
|
||||
pub const Code = Item.Code;
|
||||
|
||||
id: Id,
|
||||
seller: Account,
|
||||
item_id: Store.Id,
|
||||
quantity: u64,
|
||||
price: u64,
|
||||
created_at: f64,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !GEOrder {
|
||||
const id = try json_utils.getStringRequired(obj, "id");
|
||||
const seller = try json_utils.getStringRequired(obj, "seller");
|
||||
const code = try json_utils.getStringRequired(obj, "code");
|
||||
const quantity = try json_utils.getPositiveIntegerRequired(obj, "quantity");
|
||||
const price = try json_utils.getPositiveIntegerRequired(obj, "price");
|
||||
const created_at = try json_utils.getStringRequired(obj, "created_at");
|
||||
|
||||
return GEOrder{
|
||||
.id = try Id.fromSlice(id),
|
||||
.seller = try Account.fromSlice(seller),
|
||||
.item_id = try store.items.getOrReserveId(code),
|
||||
.quantity = quantity,
|
||||
.price = price,
|
||||
.created_at = parseDateTime(created_at) orelse return error.InvalidDataTime,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
|
||||
return try store.ge_orders.appendOrUpdate(try parse(store, obj));
|
||||
}
|
||||
31
api/schemas/move_result.zig
Normal file
31
api/schemas/move_result.zig
Normal file
@ -0,0 +1,31 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const Character = @import("./character.zig");
|
||||
const Cooldown = @import("./cooldown.zig");
|
||||
const Map = @import("./map.zig");
|
||||
|
||||
const MoveResult = @This();
|
||||
|
||||
cooldown: Cooldown,
|
||||
destination: Map,
|
||||
character: Character,
|
||||
|
||||
fn parse(store: *Store, obj: std.json.ObjectMap) !MoveResult {
|
||||
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
|
||||
const destination = json_utils.getObject(obj, "destination") orelse return error.MissingProperty;
|
||||
|
||||
return MoveResult{
|
||||
.character = try Character.parse(store, character),
|
||||
.destination = try Map.parse(store, destination),
|
||||
.cooldown = try Cooldown.parse(store, cooldown)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !MoveResult {
|
||||
const result = try parse(store, obj);
|
||||
_ = try store.characters.appendOrUpdate(result.character);
|
||||
return result;
|
||||
}
|
||||
@ -20,6 +20,12 @@ pub fn subtract(self: Position, other: Position) Position {
|
||||
return init(self.x - other.x, self.y - other.y);
|
||||
}
|
||||
|
||||
pub fn distance(self: Position, other: Position) f32 {
|
||||
const dx: f32 = @floatFromInt(self.x - other.x);
|
||||
const dy: f32 = @floatFromInt(self.y - other.y);
|
||||
return @sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
self: Position,
|
||||
comptime fmt: []const u8,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const Character = @import("./character.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const DropRate = @import("./drop_rate.zig");
|
||||
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
|
||||
@ -22,6 +23,15 @@ pub const Skill = enum {
|
||||
|
||||
pub const toString = Utils.toString;
|
||||
pub const fromString = Utils.fromString;
|
||||
|
||||
pub fn toCharacterSkill(self: Skill) Character.Skill {
|
||||
return switch (self) {
|
||||
.mining => Character.Skill.mining,
|
||||
.woodcutting => Character.Skill.woodcutting,
|
||||
.fishing => Character.Skill.fishing,
|
||||
.alchemy => Character.Skill.alchemy,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Name = std.BoundedArray(u8, 32);
|
||||
@ -29,7 +39,9 @@ pub const Name = std.BoundedArray(u8, 32);
|
||||
pub const max_code_size = 32;
|
||||
pub const Code = std.BoundedArray(u8, max_code_size);
|
||||
|
||||
pub const Drops = std.BoundedArray(DropRate, 16);
|
||||
pub const max_drops = 16;
|
||||
pub const Drop = DropRate;
|
||||
pub const Drops = std.BoundedArray(DropRate, max_drops);
|
||||
|
||||
name: Name,
|
||||
code: Code,
|
||||
|
||||
23
api/schemas/skill_info_details.zig
Normal file
23
api/schemas/skill_info_details.zig
Normal file
@ -0,0 +1,23 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const Resource = @import("./resource.zig");
|
||||
const Store = @import("../store.zig");
|
||||
const SimpleItem = @import("./simple_item.zig");
|
||||
|
||||
const SkillInfoDetails = @This();
|
||||
|
||||
pub const Items = SimpleItem.BoundedArray(Resource.max_drops);
|
||||
|
||||
xp: u64,
|
||||
items: Items,
|
||||
|
||||
pub fn parse(store: *Store, obj: std.json.ObjectMap) !SkillInfoDetails {
|
||||
const xp = try json_utils.getPositiveIntegerRequired(obj, "xp");
|
||||
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
|
||||
|
||||
return SkillInfoDetails{
|
||||
.xp = xp,
|
||||
.items = try Items.parse(store, items)
|
||||
};
|
||||
}
|
||||
31
api/schemas/skill_usage_result.zig
Normal file
31
api/schemas/skill_usage_result.zig
Normal file
@ -0,0 +1,31 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const Store = @import("../store.zig");
|
||||
const json_utils = @import("../json_utils.zig");
|
||||
const Character = @import("./character.zig");
|
||||
const Cooldown = @import("./cooldown.zig");
|
||||
const SkillInfoDetails = @import("./skill_info_details.zig");
|
||||
|
||||
const SkillUsageResult = @This();
|
||||
|
||||
cooldown: Cooldown,
|
||||
details: SkillInfoDetails,
|
||||
character: Character,
|
||||
|
||||
fn parse(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
|
||||
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||
const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty;
|
||||
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
|
||||
|
||||
return SkillUsageResult{
|
||||
.character = try Character.parse(store, character),
|
||||
.cooldown = try Cooldown.parse(store, cooldown),
|
||||
.details = try SkillInfoDetails.parse(store, details)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
|
||||
const result = try parse(store, obj);
|
||||
_ = try store.characters.appendOrUpdate(result.character);
|
||||
return result;
|
||||
}
|
||||
100
api/server.zig
100
api/server.zig
@ -22,6 +22,11 @@ const Monster = @import("./schemas/monster.zig");
|
||||
const Resource = @import("./schemas/resource.zig");
|
||||
const Position = @import("./schemas/position.zig");
|
||||
const Map = @import("./schemas/map.zig");
|
||||
const GEOrder = @import("./schemas/ge_order.zig");
|
||||
const MoveResult = @import("./schemas/move_result.zig");
|
||||
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
|
||||
pub const GatherResult = SkillUsageResult;
|
||||
pub const CraftResult = SkillUsageResult;
|
||||
const Image = Store.Image;
|
||||
|
||||
const Server = @This();
|
||||
@ -946,6 +951,101 @@ pub fn getImage(self: *Server, category: Image.Category, code: []const u8) Fetch
|
||||
return image_id;
|
||||
}
|
||||
|
||||
// https://api.artifactsmmo.com/docs/#/operations/get_ge_sell_order_grandexchange_orders__id__get
|
||||
pub fn getGEOrder(self: *Server, id: []const u8) FetchError!?Store.Id {
|
||||
const path_buff_size = comptime blk: {
|
||||
var count = 0;
|
||||
count += 22; // "/grandexchange/orders/"
|
||||
count += GEOrder.max_id_size;
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
var path_buff: [path_buff_size]u8 = undefined;
|
||||
const path = std.fmt.bufPrint(&path_buff, "/grandexchange/orders/{s}", .{ id }) catch return FetchError.InvalidPayload;
|
||||
|
||||
return try self.fetchOptionalObject(
|
||||
FetchError,
|
||||
null,
|
||||
Store.Id,
|
||||
GEOrder.parseAndAppend,
|
||||
.{ .method = .GET, .path = path, .ratelimit = .data }
|
||||
);
|
||||
}
|
||||
|
||||
// https://api.artifactsmmo.com/docs/#/operations/action_move_my__name__action_move_post
|
||||
pub fn move(self: *Server, character: []const u8, position: Position) errors.MoveError!MoveResult {
|
||||
const path_buff_size = comptime blk: {
|
||||
var count = 0;
|
||||
count += 4; // "/my/"
|
||||
count += Character.max_name_size;
|
||||
count += 12; // "/action/move"
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
var path_buff: [path_buff_size]u8 = undefined;
|
||||
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/move", .{ character }) catch return FetchError.InvalidPayload;
|
||||
|
||||
var payload_buffer: [64]u8 = undefined;
|
||||
const payload = std.fmt.bufPrint(&payload_buffer, "{{ \"x\":{}, \"y\":{} }}", .{ position.x, position.y }) catch return FetchError.InvalidPayload;
|
||||
|
||||
const result = try self.fetchObject(
|
||||
errors.MoveError,
|
||||
errors.parseMoveError,
|
||||
MoveResult,
|
||||
MoveResult.parseAndUpdate,
|
||||
.{ .method = .POST, .path = path, .ratelimit = .actions, .payload = payload }
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://api.artifactsmmo.com/docs/#/operations/action_gathering_my__name__action_gathering_post
|
||||
pub fn gather(self: *Server, character: []const u8) errors.GatherError!GatherResult {
|
||||
const path_buff_size = comptime blk: {
|
||||
var count = 0;
|
||||
count += 4; // "/my/"
|
||||
count += Character.max_name_size;
|
||||
count += 17; // "/action/gathering"
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
var path_buff: [path_buff_size]u8 = undefined;
|
||||
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/gathering", .{ character }) catch return FetchError.InvalidPayload;
|
||||
|
||||
return try self.fetchObject(
|
||||
errors.GatherError,
|
||||
errors.parseGatherError,
|
||||
GatherResult,
|
||||
GatherResult.parseAndUpdate,
|
||||
.{ .method = .POST, .path = path, .ratelimit = .actions }
|
||||
);
|
||||
}
|
||||
|
||||
// https://api.artifactsmmo.com/docs/#/operations/action_crafting_my__name__action_crafting_post
|
||||
pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) errors.CraftError!CraftResult {
|
||||
const path_buff_size = comptime blk: {
|
||||
var count = 0;
|
||||
count += 4; // "/my/"
|
||||
count += Character.max_name_size;
|
||||
count += 16; // "/action/crafting"
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
var path_buff: [path_buff_size]u8 = undefined;
|
||||
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/crafting", .{ character }) catch return FetchError.InvalidPayload;
|
||||
|
||||
var payload_buff: [256]u8 = undefined;
|
||||
const payload = std.fmt.bufPrint(&payload_buff, "{{ \"code\":\"{s}\", \"quantity\":{} }}", .{ item, quantity }) catch return FetchError.InvalidPayload;
|
||||
|
||||
return try self.fetchObject(
|
||||
errors.CraftError,
|
||||
errors.parseCraftError,
|
||||
CraftResult,
|
||||
CraftResult.parseAndUpdate,
|
||||
.{ .method = .POST, .path = path, .payload = payload, .ratelimit = .actions }
|
||||
);
|
||||
}
|
||||
|
||||
// https://api.artifactsmmo.com/docs/#/operations/generate_token_token_post
|
||||
pub fn generateToken(self: *Server, username: []const u8, password: []const u8) !AuthToken {
|
||||
const base64_encoder = std.base64.standard.Encoder;
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
const std = @import("std");
|
||||
const s2s = @import("s2s");
|
||||
const Item = @import("./schemas/item.zig");
|
||||
const SimpleItem = @import("./schemas/simple_item.zig");
|
||||
const Character = @import("./schemas/character.zig");
|
||||
const Task = @import("./schemas/task.zig");
|
||||
const Monster = @import("./schemas/monster.zig");
|
||||
const Resource = @import("./schemas/resource.zig");
|
||||
const Map = @import("./schemas/map.zig");
|
||||
const GEOrder = @import("./schemas/ge_order.zig");
|
||||
const Position = @import("./schemas/position.zig");
|
||||
|
||||
const Skin = Character.Skin;
|
||||
@ -261,7 +263,7 @@ fn Repository(comptime Object: type, comptime name_field: []const u8) type {
|
||||
if (id < self.objects.items.len) {
|
||||
return switch (self.objects.items[id]) {
|
||||
.object => |obj| @field(obj, name_field).slice(),
|
||||
.reserve => |name| name.slice(),
|
||||
.reserved => |name| name.slice(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -305,7 +307,9 @@ const Characters = Repository(Character, "name");
|
||||
const Tasks = Repository(Task, "code");
|
||||
const Monsters = Repository(Monster, "code");
|
||||
const Resources = Repository(Resource, "code");
|
||||
const GEOrders = Repository(GEOrder, "id");
|
||||
const Maps = std.ArrayListUnmanaged(Map);
|
||||
const Bank = SimpleItem.BoundedArray(64);
|
||||
|
||||
items: Items,
|
||||
characters: Characters,
|
||||
@ -314,6 +318,8 @@ monsters: Monsters,
|
||||
resources: Resources,
|
||||
images: Images,
|
||||
maps: Maps,
|
||||
bank: Bank,
|
||||
ge_orders: GEOrders,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !Store {
|
||||
const max_items = 512;
|
||||
@ -322,6 +328,7 @@ pub fn init(allocator: std.mem.Allocator) !Store {
|
||||
const max_monsters = 64;
|
||||
const max_resources = 32;
|
||||
const max_maps = 512;
|
||||
const max_ge_orders = 128;
|
||||
|
||||
var items = try Items.initCapacity(allocator, max_items);
|
||||
errdefer items.deinit(allocator);
|
||||
@ -351,6 +358,11 @@ pub fn init(allocator: std.mem.Allocator) !Store {
|
||||
var maps = try Maps.initCapacity(allocator, max_maps);
|
||||
errdefer maps.deinit(allocator);
|
||||
|
||||
var ge_orders = try GEOrders.initCapacity(allocator, max_ge_orders);
|
||||
errdefer ge_orders.deinit(allocator);
|
||||
|
||||
const bank = Bank.init();
|
||||
|
||||
return Store{
|
||||
.items = items,
|
||||
.characters = characters,
|
||||
@ -359,6 +371,8 @@ pub fn init(allocator: std.mem.Allocator) !Store {
|
||||
.resources = resources,
|
||||
.maps = maps,
|
||||
.images = images,
|
||||
.bank = bank,
|
||||
.ge_orders = ge_orders
|
||||
};
|
||||
}
|
||||
|
||||
@ -370,6 +384,7 @@ pub fn deinit(self: *Store, allocator: std.mem.Allocator) void {
|
||||
self.resources.deinit(allocator);
|
||||
self.maps.deinit(allocator);
|
||||
self.images.deinit(allocator);
|
||||
self.ge_orders.deinit(allocator);
|
||||
}
|
||||
|
||||
const SaveData = struct {
|
||||
|
||||
57
cli/main.zig
57
cli/main.zig
@ -1,3 +1,4 @@
|
||||
// zig fmt: off
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
@ -5,13 +6,12 @@ const Allocator = std.mem.Allocator;
|
||||
const Artificer = @import("artificer");
|
||||
const Api = @import("artifacts-api");
|
||||
|
||||
// zig fmt: off
|
||||
pub const std_options = .{
|
||||
.log_scope_levels = &[_]std.log.ScopeLevel{
|
||||
.{ .scope = .api, .level = .info },
|
||||
.{ .scope = .artificer, .level = .debug },
|
||||
}
|
||||
};
|
||||
// zig fmt: on
|
||||
|
||||
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
@ -37,9 +37,6 @@ pub fn main() !void {
|
||||
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
|
||||
defer allocator.free(token);
|
||||
|
||||
// var artificer = try Artificer.init(allocator, token);
|
||||
// defer artificer.deinit();
|
||||
|
||||
var store = try Api.Store.init(allocator);
|
||||
defer store.deinit(allocator);
|
||||
|
||||
@ -48,33 +45,37 @@ pub fn main() !void {
|
||||
|
||||
try server.setToken(token);
|
||||
|
||||
const resources = try server.getResources(allocator, .{});
|
||||
resources.deinit();
|
||||
|
||||
// var artificer = try Artificer.init(allocator, token);
|
||||
// defer artificer.deinit();
|
||||
|
||||
std.log.info("Prefetching server data", .{});
|
||||
{
|
||||
const status: Api.Status = try server.getStatus();
|
||||
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
|
||||
defer allocator.free(cwd_path);
|
||||
|
||||
const file = try std.fs.cwd().createFile("api-store.bin", .{});
|
||||
defer file.close();
|
||||
try store.save(status.version.slice(), file.writer());
|
||||
const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store.bin" });
|
||||
defer allocator.free(cache_path);
|
||||
|
||||
// TODO: Don't prefetch images
|
||||
try server.prefetchCached(allocator, cache_path);
|
||||
}
|
||||
|
||||
// std.log.info("Prefetching server data", .{});
|
||||
// try artificer.server.prefetchCached(cache_path);
|
||||
const character_id = (try server.getCharacter("Blondie")).?;
|
||||
|
||||
// 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();
|
||||
// }
|
||||
var artificer = try Artificer.init(allocator, &server, character_id);
|
||||
defer artificer.deinit(allocator);
|
||||
|
||||
// _ = try artificer.appendGoal(Artificer.Goal{
|
||||
// .gather = .{
|
||||
// .item = store.items.getId("sap").?,
|
||||
// .quantity = 1
|
||||
// }
|
||||
// });
|
||||
|
||||
_ = try artificer.appendGoal(Artificer.Goal{
|
||||
.craft = .{
|
||||
.item = store.items.getId("copper").?,
|
||||
.quantity = 3
|
||||
}
|
||||
});
|
||||
|
||||
std.log.info("Starting main loop", .{});
|
||||
try artificer.runUntilGoalsComplete();
|
||||
}
|
||||
|
||||
183
lib/action.zig
183
lib/action.zig
@ -1,183 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Api = @import("artifacts-api");
|
||||
const Position = Api.Position;
|
||||
const Server = Api.Server;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const Action = union(enum) {
|
||||
move: Position,
|
||||
fight,
|
||||
gather,
|
||||
deposit_gold: u64,
|
||||
deposit_item: Api.ItemQuantity,
|
||||
withdraw_item: Api.ItemQuantity,
|
||||
craft_item: Api.ItemQuantity,
|
||||
accept_task,
|
||||
|
||||
pub fn perform(self: Action, api: *Server, name: []const u8) !ActionResult {
|
||||
const log = std.log.default;
|
||||
|
||||
switch (self) {
|
||||
.fight => {
|
||||
log.debug("[{s}] attack", .{name});
|
||||
return .{
|
||||
.fight = api.actionFight(name)
|
||||
};
|
||||
},
|
||||
.move => |pos| {
|
||||
log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y});
|
||||
return .{
|
||||
.move = api.actionMove(name, pos.x, pos.y)
|
||||
};
|
||||
},
|
||||
.deposit_gold => |quantity| {
|
||||
log.debug("[{s}] deposit {} gold", .{name, quantity});
|
||||
return .{
|
||||
.deposit_gold = api.actionBankDepositGold(name, quantity)
|
||||
};
|
||||
},
|
||||
.deposit_item => |item| {
|
||||
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
|
||||
log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity});
|
||||
return .{
|
||||
.deposit_item = api.actionBankDepositItem(name, code, item.quantity)
|
||||
};
|
||||
},
|
||||
.withdraw_item => |item| {
|
||||
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
|
||||
log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity});
|
||||
return .{
|
||||
.withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity)
|
||||
};
|
||||
},
|
||||
.gather => {
|
||||
log.debug("[{s}] gather", .{name});
|
||||
return .{
|
||||
.gather = api.actionGather(name)
|
||||
};
|
||||
},
|
||||
.craft_item => |item| {
|
||||
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
|
||||
log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity});
|
||||
return .{
|
||||
.craft_item = api.actionCraft(name, code, item.quantity)
|
||||
};
|
||||
},
|
||||
.accept_task => {
|
||||
log.debug("[{s}] accept task", .{name});
|
||||
return .{
|
||||
.accept_task = api.acceptTask(name)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const ErrorResponse = enum {
|
||||
/// Something went wrong, and you probably can't reasonbly recover from it. Bail, bail!
|
||||
abort,
|
||||
|
||||
/// You probably were trying to an action a bit too early, just try again a bit later.
|
||||
retry,
|
||||
|
||||
/// Something in your logic went wrong, re-evaluate your state and do something different.
|
||||
restart,
|
||||
|
||||
/// The error can be safe ignored, continue doing the next action that you wanted.
|
||||
ignore
|
||||
};
|
||||
|
||||
pub const ActionResult = union(enum) {
|
||||
move: Api.MoveError!Server.MoveResult,
|
||||
fight: Api.FightError!Server.FightResult,
|
||||
gather: Api.GatherError!Server.GatherResult,
|
||||
deposit_gold: Api.BankDepositGoldError!Server.GoldTransactionResult,
|
||||
deposit_item: Api.BankDepositItemError!Server.ItemTransactionResult,
|
||||
withdraw_item: Api.BankWithdrawItemError!Server.ItemTransactionResult,
|
||||
craft_item: Api.CraftError!Server.CraftResult,
|
||||
accept_task: Api.AcceptTaskError!Server.AcceptTaskResult,
|
||||
|
||||
const AnyError = Server.MoveError;
|
||||
|
||||
const Tag = @typeInfo(ActionResult).Union.tag_type.?;
|
||||
|
||||
fn fieldType(comptime kind: Tag) type {
|
||||
const field_type = std.meta.fields(ActionResult)[@intFromEnum(kind)].type;
|
||||
return @typeInfo(field_type).ErrorUnion.payload;
|
||||
}
|
||||
|
||||
pub fn get(self: ActionResult, comptime kind: Tag) ?fieldType(kind) {
|
||||
return switch (self) {
|
||||
kind => |v| v catch null,
|
||||
else => null
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getError(self: ActionResult) !void {
|
||||
switch (self) {
|
||||
.fight => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.move => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.deposit_gold => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.deposit_item => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.withdraw_item => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.gather => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.craft_item => |result| {
|
||||
_ = try result;
|
||||
},
|
||||
.accept_task => |result| {
|
||||
_ = try result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getErrorResponse(self: ActionResult) ?ErrorResponse {
|
||||
self.getError() catch |err| switch (err) {
|
||||
error.CharacterIsBusy,
|
||||
error.CharacterInCooldown,
|
||||
error.BankIsBusy => return ErrorResponse.retry,
|
||||
|
||||
error.CharacterAtDestination => return ErrorResponse.ignore,
|
||||
|
||||
error.MapNotFound,
|
||||
error.CharacterIsFull,
|
||||
error.MonsterNotFound,
|
||||
error.NotEnoughSkill,
|
||||
error.ResourceNotFound,
|
||||
error.NotEnoughGold,
|
||||
error.BankNotFound,
|
||||
error.ItemNotFound,
|
||||
error.NotEnoughItems,
|
||||
error.RecipeNotFound,
|
||||
error.AlreadyHasTask,
|
||||
error.TaskMasterNotFound,
|
||||
error.WorkshopNotFound => return ErrorResponse.restart,
|
||||
|
||||
error.CharacterNotFound,
|
||||
error.ServerUnavailable,
|
||||
error.RequestFailed,
|
||||
error.ParseFailed,
|
||||
error.OutOfMemory => return ErrorResponse.abort
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
const ActionTag = @typeInfo(Action).Union.tag_type.?;
|
||||
const ResultTag = @typeInfo(ActionResult).Union.tag_type.?;
|
||||
|
||||
assert(std.meta.fields(ActionTag).len == std.meta.fields(ResultTag).len);
|
||||
}
|
||||
104
lib/brain.zig
104
lib/brain.zig
@ -1,104 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Api = @import("artifacts-api");
|
||||
const Server = Api.Server;
|
||||
const Position = Api.Position;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const CharacterTask = @import("./task.zig").Task;
|
||||
const QueuedAction = @import("./action.zig").Action;
|
||||
const QueuedActionResult = @import("./action.zig").ActionResult;
|
||||
|
||||
const Brain = @This();
|
||||
|
||||
name: []const u8,
|
||||
action_queue: std.ArrayList(QueuedAction),
|
||||
task: ?CharacterTask = null,
|
||||
paused_until: ?i64 = null, // ms
|
||||
|
||||
pub fn init(allocator: Allocator, name: []const u8) !Brain {
|
||||
return Brain{
|
||||
.name = try allocator.dupe(u8, name),
|
||||
.action_queue = std.ArrayList(QueuedAction).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Brain) void {
|
||||
const allocator = self.action_queue.allocator;
|
||||
allocator.free(self.name);
|
||||
self.action_queue.deinit();
|
||||
}
|
||||
|
||||
pub fn performNextAction(self: *Brain, api: *Server) !void {
|
||||
const log = std.log.default;
|
||||
assert(self.action_queue.items.len > 0);
|
||||
|
||||
const retry_delay = 500; // 500ms
|
||||
|
||||
const next_action = self.action_queue.items[0];
|
||||
const action_result = try next_action.perform(api, self.name);
|
||||
|
||||
if (action_result.getErrorResponse()) |error_response| {
|
||||
switch (error_response) {
|
||||
.retry => {
|
||||
self.paused_until = std.time.milliTimestamp() + retry_delay;
|
||||
log.warn("[{s}] retry action", .{self.name});
|
||||
return;
|
||||
},
|
||||
.restart => {
|
||||
log.warn("[{s}] clear action queue", .{self.name});
|
||||
self.action_queue.clearAndFree();
|
||||
return;
|
||||
},
|
||||
.abort => {
|
||||
log.warn("[{s}] abort action {s}", .{ self.name, @tagName(next_action) });
|
||||
try action_result.getError();
|
||||
|
||||
// The error above should always return
|
||||
unreachable;
|
||||
},
|
||||
.ignore => { },
|
||||
}
|
||||
}
|
||||
|
||||
_ = self.action_queue.orderedRemove(0);
|
||||
|
||||
if (self.task) |*task| {
|
||||
task.onActionCompleted(action_result);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step(self: *Brain, api: *Api.Server) !void {
|
||||
if (self.paused_until) |paused_until| {
|
||||
if (std.time.milliTimestamp() < paused_until) {
|
||||
return;
|
||||
}
|
||||
self.paused_until = null;
|
||||
}
|
||||
|
||||
if (self.action_queue.items.len > 0) return;
|
||||
|
||||
if (self.task) |task| {
|
||||
if (task.isComplete()) {
|
||||
// if (try brain.depositItemsToBank(&self.server)) {
|
||||
// continue;
|
||||
// }
|
||||
self.task = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.task) |task| {
|
||||
try task.queueActions(api, self.name, &self.action_queue);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cooldown(self: *Brain, api: *Server) i64 {
|
||||
const character = api.store.getCharacter(self.name).?;
|
||||
const cooldown_expiration: i64 = @intFromFloat(character.cooldown_expiration * std.time.ms_per_s);
|
||||
|
||||
if (self.paused_until) |pause_until| {
|
||||
return @max(cooldown_expiration, pause_until);
|
||||
} else {
|
||||
return cooldown_expiration;
|
||||
}
|
||||
}
|
||||
67
lib/craft_goal.zig
Normal file
67
lib/craft_goal.zig
Normal file
@ -0,0 +1,67 @@
|
||||
// zig fmt: off
|
||||
const Api = @import("artifacts-api");
|
||||
const Artificer = @import("./root.zig");
|
||||
|
||||
const Goal = @This();
|
||||
|
||||
item: Api.Store.Id,
|
||||
quantity: u64,
|
||||
|
||||
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void {
|
||||
const store = artificer.server.store;
|
||||
const character = store.characters.get(artificer.character).?;
|
||||
|
||||
if (self.quantity == 0) {
|
||||
artificer.removeGoal(goal_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const item = store.items.get(self.item).?;
|
||||
const craft = item.craft.?;
|
||||
|
||||
const skill = craft.skill.toCharacterSkill();
|
||||
if (character.skills.get(skill).level < craft.level) {
|
||||
return error.SkillTooLow;
|
||||
}
|
||||
|
||||
const craft_multiples: u64 = @intFromFloat(@ceil(
|
||||
@as(f32, @floatFromInt(self.quantity)) /
|
||||
@as(f32, @floatFromInt(craft.quantity))
|
||||
));
|
||||
|
||||
for (craft.items.items.slice()) |craft_item| {
|
||||
const inventory_item_quantity = character.inventory.getQuantity(craft_item.id);
|
||||
if (inventory_item_quantity < craft_item.quantity * craft_multiples) {
|
||||
return error.NotEnoughItems;
|
||||
}
|
||||
}
|
||||
|
||||
const workshop_position = artificer.findNearestWorkstation(craft.skill).?;
|
||||
if (!workshop_position.eql(character.position)) {
|
||||
artificer.queued_actions.appendAssumeCapacity(.{
|
||||
.goal = goal_id,
|
||||
.action = .{ .move = workshop_position }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
artificer.queued_actions.appendAssumeCapacity(.{
|
||||
.goal = goal_id,
|
||||
.action = .{ .craft = .{ .item = self.item, .quantity = self.quantity } }
|
||||
});
|
||||
}
|
||||
|
||||
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
|
||||
_ = goal_id;
|
||||
|
||||
if (result == .craft) {
|
||||
const craft_result = result.craft;
|
||||
const craft_quantity = craft_result.details.items.getQuantity(self.item);
|
||||
|
||||
if (self.quantity > craft_quantity) {
|
||||
self.quantity -= craft_quantity;
|
||||
} else {
|
||||
self.quantity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
lib/gather_goal.zig
Normal file
51
lib/gather_goal.zig
Normal file
@ -0,0 +1,51 @@
|
||||
// zig fmt: off
|
||||
const Api = @import("artifacts-api");
|
||||
const Artificer = @import("./root.zig");
|
||||
|
||||
const Goal = @This();
|
||||
|
||||
item: Api.Store.Id,
|
||||
quantity: u64,
|
||||
|
||||
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void {
|
||||
const store = artificer.server.store;
|
||||
const character = store.characters.get(artificer.character).?;
|
||||
|
||||
if (self.quantity == 0) {
|
||||
artificer.removeGoal(goal_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const resource_id = artificer.findBestResourceWithItem(self.item) orelse @panic("Failed to find resource with item");
|
||||
|
||||
const map_position: Api.Position = artificer.findNearestMapWithResource(resource_id) orelse @panic("Map with resource not found");
|
||||
|
||||
if (!map_position.eql(character.position)) {
|
||||
artificer.queued_actions.appendAssumeCapacity(.{
|
||||
.goal = goal_id,
|
||||
.action = .{ .move = map_position }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
artificer.queued_actions.appendAssumeCapacity(.{
|
||||
.goal = goal_id,
|
||||
.action = .{ .gather = {} }
|
||||
});
|
||||
}
|
||||
|
||||
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
|
||||
_ = goal_id;
|
||||
|
||||
if (result == .gather) {
|
||||
const gather_result = result.gather;
|
||||
const gather_quantity = gather_result.details.items.getQuantity(self.item);
|
||||
|
||||
if (self.quantity > gather_quantity) {
|
||||
self.quantity -= gather_quantity;
|
||||
} else {
|
||||
self.quantity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
414
lib/root.zig
414
lib/root.zig
@ -3,134 +3,326 @@ 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 GatherGoal = @import("gather_goal.zig");
|
||||
const CraftGoal = @import("craft_goal.zig");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.artificer);
|
||||
|
||||
const Artificer = @This();
|
||||
|
||||
pub const GoalId = packed struct {
|
||||
const Generation = u5;
|
||||
const Index = u11;
|
||||
|
||||
generation: Generation,
|
||||
index: Index
|
||||
};
|
||||
|
||||
const max_goals = std.math.maxInt(GoalId.Index);
|
||||
|
||||
const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
|
||||
const server_down_retry_interval = 5; // minutes
|
||||
|
||||
pub const Goal = union(enum) {
|
||||
gather: GatherGoal,
|
||||
craft: CraftGoal,
|
||||
|
||||
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void {
|
||||
switch (self.*) {
|
||||
.gather => |*gather| gather.tick(goal_id, artificer),
|
||||
.craft => |*craft| try craft.tick(goal_id, artificer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
|
||||
switch (self.*) {
|
||||
.gather => |*gather| gather.onActionCompleted(goal_id, result),
|
||||
.craft => |*craft| craft.onActionCompleted(goal_id, result),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const GoalSlot = struct {
|
||||
generation: GoalId.Generation = 0,
|
||||
goal: ?Goal = null,
|
||||
};
|
||||
|
||||
pub const Action = union(enum) {
|
||||
move: Api.Position,
|
||||
gather,
|
||||
craft: struct {
|
||||
item: Api.Store.Id,
|
||||
quantity: u64
|
||||
}
|
||||
};
|
||||
|
||||
pub const ActionResult = union(enum) {
|
||||
move: Api.MoveResult,
|
||||
gather: Api.GatherResult,
|
||||
craft: Api.CraftResult,
|
||||
};
|
||||
|
||||
const ActionSlot = struct {
|
||||
goal: GoalId,
|
||||
action: Action,
|
||||
};
|
||||
|
||||
const QueuedActions = std.ArrayListUnmanaged(ActionSlot);
|
||||
|
||||
server: *Api.Server,
|
||||
// characters: std.ArrayList(Brain),
|
||||
// task_graph: TaskGraph,
|
||||
character: Api.Store.Id,
|
||||
goal_slots: []GoalSlot,
|
||||
queued_actions: QueuedActions,
|
||||
|
||||
// paused_until: ?i64 = null, // ms
|
||||
pub fn init(allocator: Allocator, server: *Api.Server, character: Api.Store.Id) !Artificer {
|
||||
const max_queued_actions = 16;
|
||||
|
||||
pub fn init(allocator: Allocator, server: *Api.Server) !Artificer {
|
||||
// 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));
|
||||
// }
|
||||
const goal_slots = try allocator.alloc(GoalSlot, max_goals);
|
||||
errdefer allocator.free(goal_slots);
|
||||
@memset(goal_slots, .{});
|
||||
|
||||
var queued_actions = try QueuedActions.initCapacity(allocator, max_queued_actions);
|
||||
errdefer queued_actions.deinit(allocator);
|
||||
|
||||
_ = allocator;
|
||||
return Artificer{
|
||||
.server = server,
|
||||
// .characters = characters,
|
||||
// .task_graph = TaskGraph.init(allocator),
|
||||
.goal_slots = goal_slots,
|
||||
.character = character,
|
||||
.queued_actions = queued_actions
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Artificer) void {
|
||||
_ = self;
|
||||
// for (self.characters.items) |brain| {
|
||||
// brain.deinit();
|
||||
// }
|
||||
// self.characters.deinit();
|
||||
// self.server.deinit();
|
||||
pub fn deinit(self: *Artificer, allocator: Allocator) void {
|
||||
allocator.free(self.goal_slots);
|
||||
self.queued_actions.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn appendGoal(self: *Artificer, goal: Goal) !GoalId {
|
||||
for (0.., self.goal_slots) |index, *goal_slot| {
|
||||
if (goal_slot.goal != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (goal_slot.generation == std.math.maxInt(GoalId.Generation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
goal_slot.goal = goal;
|
||||
return GoalId{
|
||||
.index = @intCast(index),
|
||||
.generation = goal_slot.generation
|
||||
};
|
||||
}
|
||||
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn removeGoal(self: *Artificer, id: GoalId) void {
|
||||
if (self.getGoal(id)) |goal_slot| {
|
||||
goal_slot.* = .{
|
||||
.generation = goal_slot.generation + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getGoal(self: *Artificer, id: GoalId) ?*GoalSlot {
|
||||
const slot = &self.goal_slots[id.index];
|
||||
|
||||
if (slot.generation != id.generation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (slot.goal == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
pub fn findBestResourceWithItem(self: *Artificer, item: Api.Store.Id) ?Api.Store.Id {
|
||||
const store = self.server.store;
|
||||
const character = store.characters.get(self.character).?;
|
||||
|
||||
var best_resource: ?Api.Store.Id = null;
|
||||
var best_rate: u64 = 0;
|
||||
|
||||
for (0.., store.resources.objects.items) |resource_id, optional_resource| {
|
||||
if (optional_resource != .object) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resource = optional_resource.object;
|
||||
|
||||
const skill = resource.skill.toCharacterSkill();
|
||||
const character_skill_level = character.skills.get(skill).level;
|
||||
if (character_skill_level < resource.level) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (resource.drops.slice()) |_drop| {
|
||||
const drop: Api.Resource.Drop = _drop;
|
||||
if (drop.item != item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The lower the `drop.rate` the better
|
||||
if (best_resource == null or best_rate > drop.rate) {
|
||||
best_resource = resource_id;
|
||||
best_rate = drop.rate;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return best_resource;
|
||||
}
|
||||
|
||||
pub fn findNearestMapWithResource(self: *Artificer, resource: Api.Store.Id) ?Api.Position {
|
||||
const store = self.server.store;
|
||||
const character = store.characters.get(self.character).?;
|
||||
const resource_code = store.resources.get(resource).?.code.slice();
|
||||
|
||||
var nearest_position: ?Api.Position = null;
|
||||
for (store.maps.items) |map| {
|
||||
const content = map.content orelse continue;
|
||||
|
||||
if (content.type != .resource) {
|
||||
continue;
|
||||
}
|
||||
if (!std.mem.eql(u8, resource_code, content.code.slice())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nearest_position == null or map.position.distance(character.position) < map.position.distance(nearest_position.?)) {
|
||||
nearest_position = map.position;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest_position;
|
||||
}
|
||||
|
||||
pub fn findNearestWorkstation(self: *Artificer, skill: Api.Craft.Skill) ?Api.Position {
|
||||
const store = self.server.store;
|
||||
const character = store.characters.get(self.character).?;
|
||||
const skill_name = skill.toString();
|
||||
|
||||
var nearest_position: ?Api.Position = null;
|
||||
for (store.maps.items) |map| {
|
||||
const content = map.content orelse continue;
|
||||
|
||||
if (content.type != .workshop) {
|
||||
continue;
|
||||
}
|
||||
if (!std.mem.eql(u8, skill_name, content.code.slice())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nearest_position == null or map.position.distance(character.position) < map.position.distance(nearest_position.?)) {
|
||||
nearest_position = map.position;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest_position;
|
||||
}
|
||||
|
||||
fn timeUntilCooldownExpires(self: *Artificer) u64 {
|
||||
const store = self.server.store;
|
||||
|
||||
const character = store.characters.get(self.character).?;
|
||||
if (character.cooldown_expiration) |cooldown_expiration| {
|
||||
const cooldown_expiration_ns: i64 = @intFromFloat(cooldown_expiration * std.time.ns_per_s);
|
||||
const now = std.time.nanoTimestamp();
|
||||
if (cooldown_expiration_ns > now) {
|
||||
return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn getGoalCount(self: *Artificer) u32 {
|
||||
var count: u32 = 0;
|
||||
|
||||
for (self.goal_slots) |goal_slot| {
|
||||
if (goal_slot.goal == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
pub fn tick(self: *Artificer) !void {
|
||||
_ = self;
|
||||
const store = self.server.store;
|
||||
|
||||
if (self.queued_actions.items.len > 0) {
|
||||
const expires_in = self.timeUntilCooldownExpires();
|
||||
if (expires_in > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const character = store.characters.get(self.character).?;
|
||||
const action_slot = self.queued_actions.orderedRemove(0);
|
||||
const action_result = switch (action_slot.action) {
|
||||
.move => |position| ActionResult{
|
||||
.move = try self.server.move(character.name.slice(), position)
|
||||
},
|
||||
.gather => ActionResult{
|
||||
.gather = try self.server.gather(character.name.slice())
|
||||
},
|
||||
.craft => |craft| ActionResult{
|
||||
.craft = try self.server.craft(character.name.slice(), store.items.getName(craft.item).?, craft.quantity)
|
||||
}
|
||||
};
|
||||
|
||||
if (self.getGoal(action_slot.goal)) |goal_slot| {
|
||||
const goal = &goal_slot.goal.?;
|
||||
goal.onActionCompleted(action_slot.goal, action_result);
|
||||
}
|
||||
|
||||
} else {
|
||||
for (0.., self.goal_slots) |index, *goal_slot| {
|
||||
if (goal_slot.goal == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const goal = &(goal_slot.*.goal orelse continue);
|
||||
|
||||
const goal_id = GoalId{
|
||||
.index = @intCast(index),
|
||||
.generation = goal_slot.generation
|
||||
};
|
||||
|
||||
try goal.tick(goal_id, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
pub fn runUntilGoalsComplete(self: *Artificer) !void {
|
||||
while (self.getGoalCount() > 0) {
|
||||
const expires_in = self.timeUntilCooldownExpires();
|
||||
if (expires_in > 0) {
|
||||
log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
|
||||
std.time.sleep(expires_in);
|
||||
}
|
||||
|
||||
try self.tick();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runForever(self: *Artificer) !void {
|
||||
while (true) {
|
||||
const expires_in = self.timeUntilCooldownExpires();
|
||||
if (expires_in > 0) {
|
||||
log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
|
||||
std.time.sleep(expires_in);
|
||||
log.debug("Finished sleeping", .{});
|
||||
}
|
||||
|
||||
try self.tick();
|
||||
}
|
||||
}
|
||||
|
||||
302
lib/task.zig
302
lib/task.zig
@ -1,302 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Api = @import("artifacts-api");
|
||||
const Position = Api.Position;
|
||||
|
||||
const Action = @import("./action.zig").Action;
|
||||
const ActionResult = @import("./action.zig").ActionResult;
|
||||
const CodeId = Api.CodeId;
|
||||
const ItemQuantity = Api.ItemQuantity;
|
||||
|
||||
const bank_position = Position{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
|
||||
|
||||
const task_master_position = Position{ .x = 1, .y = 2 }; // TODO: Figure this out dynamically
|
||||
|
||||
pub const UntilCondition = union(enum) {
|
||||
xp: u64,
|
||||
item: Api.ItemQuantity,
|
||||
quantity: u64,
|
||||
|
||||
fn isComplete(self: UntilCondition, progress: u64) bool {
|
||||
return switch (self) {
|
||||
.xp => |xp| progress >= xp,
|
||||
.item => |item| progress >= item.quantity,
|
||||
.quantity => |quantity| progress >= quantity,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Task = union(enum) {
|
||||
fight: struct {
|
||||
at: Position,
|
||||
until: UntilCondition,
|
||||
progress: u64 = 0,
|
||||
},
|
||||
gather: struct {
|
||||
at: Position,
|
||||
until: UntilCondition,
|
||||
progress: u64 = 0,
|
||||
},
|
||||
craft: struct {
|
||||
at: Position,
|
||||
target: Api.ItemQuantity,
|
||||
progress: u64 = 0,
|
||||
},
|
||||
accept_task: struct {
|
||||
done: bool = false
|
||||
},
|
||||
|
||||
pub fn isComplete(self: Task) bool {
|
||||
return switch (self) {
|
||||
.fight => |args| args.until.isComplete(args.progress),
|
||||
.gather => |args| args.until.isComplete(args.progress),
|
||||
.craft => |args| args.progress >= args.target.quantity,
|
||||
.accept_task => |args| args.done
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onActionCompleted(self: *Task, result: ActionResult) void {
|
||||
switch (self.*) {
|
||||
.fight => |*args| {
|
||||
if (result.get(.fight)) |r| {
|
||||
const fight_result: Api.Server.FightResult = r;
|
||||
|
||||
switch (args.until) {
|
||||
.xp => {
|
||||
args.progress += fight_result.fight.xp;
|
||||
},
|
||||
.item => {
|
||||
const drops = fight_result.fight.drops;
|
||||
args.progress += drops.getQuantity(args.until.item.id);
|
||||
},
|
||||
.quantity => {
|
||||
args.progress += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gather => |*args| {
|
||||
if (result.get(.gather)) |r| {
|
||||
const gather_result: Api.Server.GatherResult = r;
|
||||
|
||||
switch (args.until) {
|
||||
.xp => {
|
||||
args.progress += gather_result.details.xp;
|
||||
},
|
||||
.item => {
|
||||
const items = gather_result.details.items;
|
||||
args.progress += items.getQuantity(args.until.item.id);
|
||||
},
|
||||
.quantity => {
|
||||
args.progress += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.craft => |*args| {
|
||||
if (result.get(.craft_item)) |r| {
|
||||
const craft_result: Api.Server.CraftResult = r;
|
||||
const items = craft_result.details.items;
|
||||
|
||||
args.progress += items.getQuantity(args.target.id);
|
||||
}
|
||||
},
|
||||
.accept_task => |*args| {
|
||||
if (result.get(.accept_task)) |_| {
|
||||
args.done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queueActions(self: Task, api: *Api.Server, name: []const u8, action_queue: *std.ArrayList(Action)) !void {
|
||||
const ctx = TaskContext{
|
||||
.api = api,
|
||||
.name = name,
|
||||
.action_queue = action_queue
|
||||
};
|
||||
|
||||
switch (self) {
|
||||
.fight => |args| {
|
||||
try ctx.fightRoutine(args.at);
|
||||
},
|
||||
.gather => |args| {
|
||||
try ctx.gatherRoutine(args.at);
|
||||
},
|
||||
.craft => |args| {
|
||||
try ctx.craftRoutine(args.at, args.target.id, args.target.quantity);
|
||||
},
|
||||
.accept_task => {
|
||||
if (try ctx.moveIfNeeded(task_master_position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try ctx.action_queue.append(.{ .accept_task = {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TaskContext = struct {
|
||||
api: *Api.Server,
|
||||
name: []const u8,
|
||||
action_queue: *std.ArrayList(Action),
|
||||
|
||||
fn getCharacter(self: TaskContext) Api.Character {
|
||||
return self.api.store.getCharacter(self.name).?;
|
||||
}
|
||||
|
||||
fn moveIfNeeded(self: TaskContext, pos: Position) !bool {
|
||||
const character = self.getCharacter();
|
||||
|
||||
if (character.position.eql(pos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try self.action_queue.append(.{ .move = pos });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn depositItemsToBank(self: TaskContext) !bool {
|
||||
var character = self.getCharacter();
|
||||
const action_queue = self.action_queue;
|
||||
|
||||
// Deposit items and gold to bank if full
|
||||
if (character.getItemCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!character.position.eql(bank_position)) {
|
||||
try action_queue.append(.{ .move = bank_position });
|
||||
}
|
||||
|
||||
for (character.inventory.slice()) |slot| {
|
||||
try action_queue.append(.{
|
||||
.deposit_item = .{ .id = slot.id, .quantity = slot.quantity }
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn depositIfFull(self: TaskContext) !bool {
|
||||
const character = self.getCharacter();
|
||||
if (character.getItemCount() < character.inventory_max_items) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = try depositItemsToBank(self);
|
||||
|
||||
if (character.gold > 0) {
|
||||
try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn fightRoutine(self: TaskContext, enemy_position: Position) !void {
|
||||
if (try self.depositIfFull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (try self.moveIfNeeded(enemy_position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try self.action_queue.append(.{ .fight = {} });
|
||||
}
|
||||
|
||||
fn gatherRoutine(self: TaskContext, resource_position: Position) !void {
|
||||
if (try self.depositIfFull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (try self.moveIfNeeded(resource_position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try self.action_queue.append(.{ .gather = {} });
|
||||
}
|
||||
|
||||
fn withdrawFromBank(self: TaskContext, items: []const ItemQuantity) !bool {
|
||||
const character = self.getCharacter();
|
||||
|
||||
var has_all_items = true;
|
||||
for (items) |item_quantity| {
|
||||
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
|
||||
if(inventory_quantity < item_quantity.quantity) {
|
||||
has_all_items = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (has_all_items) return false;
|
||||
|
||||
if (try self.moveIfNeeded(bank_position)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (items) |item_quantity| {
|
||||
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
|
||||
if(inventory_quantity < item_quantity.quantity) {
|
||||
try self.action_queue.append(.{ .withdraw_item = .{
|
||||
.id = item_quantity.id,
|
||||
.quantity = item_quantity.quantity - inventory_quantity,
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn craftItem(self: TaskContext, workstation: Position, id: CodeId, quantity: u64) !bool {
|
||||
var character = self.getCharacter();
|
||||
|
||||
const inventory_quantity = character.inventory.getQuantity(id);
|
||||
if (inventory_quantity >= quantity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (try self.moveIfNeeded(workstation)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try self.action_queue.append(.{ .craft_item = .{
|
||||
.id = id,
|
||||
.quantity = quantity - inventory_quantity
|
||||
}});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn craftRoutine(self: TaskContext, workstation: Position, id: CodeId, quantity: u64) !void {
|
||||
var character = self.getCharacter();
|
||||
const inventory_quantity = character.inventory.getQuantity(id);
|
||||
if (inventory_quantity >= quantity) {
|
||||
if (try self.depositItemsToBank()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const code = self.api.store.getCode(id) orelse return error.InvalidItemId;
|
||||
const target_item = try self.api.getItem(code) orelse return error.ItemNotFound;
|
||||
if (target_item.craft == null) {
|
||||
return error.NotCraftable;
|
||||
}
|
||||
|
||||
const recipe = target_item.craft.?;
|
||||
|
||||
var needed_items = recipe.items;
|
||||
for (needed_items.slice()) |*needed_item| {
|
||||
needed_item.quantity *= quantity;
|
||||
}
|
||||
|
||||
if (try self.withdrawFromBank(needed_items.slice())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (try self.craftItem(workstation, id, quantity)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,197 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Api = @import("artifacts-api");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const TaskGraph = @This();
|
||||
const CharacterTask = @import("./task.zig").Task;
|
||||
|
||||
const TaskNodeId = u16;
|
||||
const TaskNode = struct {
|
||||
const Dependencies = std.BoundedArray(TaskNodeId, 8);
|
||||
const MissingItems = Api.BoundedSlotsArray(8);
|
||||
|
||||
task: CharacterTask,
|
||||
dependencies: Dependencies = Dependencies.init(0) catch unreachable,
|
||||
missing_items: MissingItems = MissingItems.init(),
|
||||
};
|
||||
|
||||
const Nodes = std.ArrayList(TaskNode);
|
||||
|
||||
nodes: Nodes,
|
||||
|
||||
pub fn init(allocator: Allocator) TaskGraph {
|
||||
return TaskGraph{ .nodes = Nodes.init(allocator) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: TaskGraph) void {
|
||||
self.nodes.deinit();
|
||||
}
|
||||
|
||||
fn get(self: *TaskGraph, id: TaskNodeId) *TaskNode {
|
||||
return &self.nodes.items[id];
|
||||
}
|
||||
|
||||
fn addTask(self: *TaskGraph, node: TaskNode) !TaskNodeId {
|
||||
try self.nodes.append(node);
|
||||
return @intCast(self.nodes.items.len-1);
|
||||
}
|
||||
|
||||
fn addFightTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
|
||||
const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
|
||||
const monsters = try api.getMonsters(.{ .drop = item_code });
|
||||
defer monsters.deinit();
|
||||
|
||||
if (monsters.items.len == 0) return error.ResourceNotFound;
|
||||
if (monsters.items.len > 1) std.log.warn("Multiple monsters exist for target item", .{});
|
||||
const monster_code = monsters.items[0].code;
|
||||
|
||||
const resource_maps = try self.api.getMaps(.{ .code = monster_code });
|
||||
defer resource_maps.deinit();
|
||||
|
||||
// This monster currently doesn't exist on the map. Probably only spawns in certain situations.
|
||||
if (resource_maps.items.len == 0) return error.MapNotFound;
|
||||
|
||||
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{});
|
||||
const resource_map = resource_maps.items[0];
|
||||
|
||||
return try self.addTask(TaskNode{
|
||||
.task = .{
|
||||
.fight = .{
|
||||
.at = resource_map.position,
|
||||
.until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn addGatherTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
|
||||
const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
|
||||
const resources = try api.getResources(.{ .drop = item_code });
|
||||
defer resources.deinit();
|
||||
|
||||
if (resources.items.len == 0) return error.ResourceNotFound;
|
||||
if (resources.items.len > 1) std.log.warn("Multiple resources exist for target item", .{});
|
||||
const resource_code = resources.items[0].code;
|
||||
|
||||
const resource_maps = try self.api.getMaps(.{ .code = resource_code });
|
||||
defer resource_maps.deinit();
|
||||
|
||||
if (resource_maps.items.len == 0) return error.MapNotFound;
|
||||
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for resource", .{});
|
||||
const resource_map = resource_maps.items[0];
|
||||
|
||||
return try self.addTask(TaskNode{
|
||||
.task = .{
|
||||
.gather = .{
|
||||
.at = resource_map.position,
|
||||
.until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn addCraftTaskShallow(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
|
||||
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
|
||||
const recipe = item.craft orelse return error.RecipeNotFound;
|
||||
|
||||
const skill_str = Api.Server.SkillUtils.toString(recipe.skill);
|
||||
const workshop_maps = try self.api.getMaps(.{ .code = skill_str, .type = .workshop });
|
||||
defer workshop_maps.deinit();
|
||||
|
||||
if (workshop_maps.items.len == 0) return error.WorkshopNotFound;
|
||||
if (workshop_maps.items.len > 1) std.log.warn("Multiple workshop locations exist", .{});
|
||||
|
||||
return try self.addTask(TaskNode{
|
||||
.task = .{
|
||||
.craft = .{
|
||||
.at = workshop_maps.items[0].position,
|
||||
.target = Api.ItemQuantity.init(item_id, quantity)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn addCraftTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
|
||||
const node_id = try self.addCraftTaskShallow(api, item_id, quantity);
|
||||
var node = self.get(node_id);
|
||||
|
||||
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
|
||||
const recipe = item.craft orelse return error.RecipeNotFound;
|
||||
|
||||
const craft_count = recipe.quantity;
|
||||
|
||||
for (recipe.items.slots.constSlice()) |material| {
|
||||
const needed_quantity = material.quantity * craft_count;
|
||||
|
||||
if (try self.addAutoTask(api, material.id, needed_quantity)) |dependency_id| {
|
||||
try node.dependencies.append(dependency_id);
|
||||
} else {
|
||||
try node.missing_items.add(material.id, needed_quantity);
|
||||
}
|
||||
}
|
||||
|
||||
return node_id;
|
||||
}
|
||||
|
||||
// TODO: Remove `anyerror` from function declaration
|
||||
fn addAutoTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) anyerror!?TaskNodeId {
|
||||
const item = (try self.api.getItemById(item_id)) orelse return error.ItemNotFound;
|
||||
|
||||
if (item.craft != null) {
|
||||
return try self.addCraftTask(api, item_id, quantity);
|
||||
} else if (item.type == .resource) {
|
||||
const eql = std.mem.eql;
|
||||
if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) {
|
||||
return try self.addGatherTask(api, item_id, quantity);
|
||||
} else if (eql(u8, item.subtype, "mob")) {
|
||||
return try self.addFightTask(api, item_id, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn printTask(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId) void {
|
||||
self.printTaskLevel(api, node_id, 0);
|
||||
}
|
||||
|
||||
fn writeIdentation(level: u32) void {
|
||||
const mutex = std.debug.getStderrMutex();
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
stderr.writeBytesNTimes(" ", level) catch return;
|
||||
}
|
||||
|
||||
fn printTaskLevel(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId, level: u32) void {
|
||||
const node = self.get(node_id);
|
||||
const print = std.debug.print;
|
||||
|
||||
writeIdentation(level);
|
||||
switch (node.task) {
|
||||
.fight => |args| {
|
||||
const target_item = args.until.item;
|
||||
const item = api.store.getCode(target_item.id).?;
|
||||
print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
|
||||
},
|
||||
.gather => |args| {
|
||||
const target_item = args.until.item;
|
||||
const item = api.store.getCode(target_item.id).?;
|
||||
print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
|
||||
},
|
||||
.craft => |args| {
|
||||
const item = api.store.getCode(args.target.id).?;
|
||||
print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at});
|
||||
},
|
||||
}
|
||||
|
||||
for (node.dependencies.constSlice()) |dependency| {
|
||||
self.printTaskLevel(dependency, level + 1);
|
||||
}
|
||||
for (node.missing_items.slots.constSlice()) |slot| {
|
||||
const item_code = api.getItemCode(slot.id).?;
|
||||
writeIdentation(level+1);
|
||||
print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user