From d7032977a20449496781c4fa7ed6303f1608bd27 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Thu, 1 Aug 2024 00:56:33 +0300 Subject: [PATCH] basic combat loop --- src/artifacts.zig | 210 ++++++++++++++++++++++++++++++++++++---------- src/main.zig | 112 +++++++++++++++++++++++-- 2 files changed, 269 insertions(+), 53 deletions(-) diff --git a/src/artifacts.zig b/src/artifacts.zig index 12fb3b2..8bd1bc2 100644 --- a/src/artifacts.zig +++ b/src/artifacts.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const json = std.json; const Allocator = std.mem.Allocator; @@ -116,6 +117,37 @@ pub const Character = struct { } }; + pub const Inventory = struct { + const slots_count = 20; + + pub const InventorySlot = struct { + code: []const u8, + quantity: i64, + + fn parse(allocator: Allocator, slot_obj: json.ObjectMap) !InventorySlot { + return InventorySlot{ + .code = (try dupeJsonString(allocator, slot_obj, "code")) orelse return error.MissingProperty, + .quantity = getJsonInteger(slot_obj, "quantity") orelse return error.MissingProperty, + }; + } + }; + + slots: [slots_count]InventorySlot, + + fn parse(allocator: Allocator, slots_array: json.Array) !Inventory { + assert(slots_array.items.len == Inventory.slots_count); + + var inventory: Inventory = undefined; + + for (0.., slots_array.items) |i, slot_value| { + const slot_obj = asJsonObject(slot_value) orelse return error.InvalidType; + inventory.slots[i] = try InventorySlot.parse(allocator, slot_obj); + } + + return inventory; + } + }; + arena: *std.heap.ArenaAllocator, name: []u8, @@ -146,6 +178,9 @@ pub const Character = struct { equipment: Equipment, + inventory_max_items: i64, + inventory: Inventory, + fn parse(child_allocator: Allocator, obj: json.ObjectMap) !Character { var arena = try child_allocator.create(std.heap.ArenaAllocator); errdefer child_allocator.destroy(arena); @@ -155,6 +190,8 @@ pub const Character = struct { const allocator = arena.allocator(); + const inventory = getJsonArray(obj, "inventory") orelse return error.MissingProperty; + return Character{ .arena = arena, .account = try dupeJsonString(allocator, obj, "account"), @@ -184,7 +221,10 @@ pub const Character = struct { .earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"), .air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"), - .equipment = try Equipment.parse(allocator, obj) + .equipment = try Equipment.parse(allocator, obj), + + .inventory_max_items = getJsonInteger(obj, "inventory_max_items") orelse return error.MissingProperty, + .inventory = try Inventory.parse(allocator, inventory) }; } @@ -193,6 +233,14 @@ pub const Character = struct { self.arena.deinit(); child_allocator.destroy(self.arena); } + + pub fn getItemCount(self: *const Character) u32 { + var count: u32 = 0; + for (self.inventory.slots) |slot| { + count += @intCast(slot.quantity); + } + return count; + } }; pub const CharacterList = struct { @@ -267,8 +315,8 @@ pub const Cooldown = struct { const reason = getJsonString(obj, "reason") orelse return error.MissingProperty; return Cooldown{ .allocator = allocator, - .total_seconds = getJsonInteger(obj, "totalSeconds") orelse return error.MissingProperty, - .remaining_seconds = getJsonInteger(obj, "remainingSeconds") orelse return error.MissingProperty, + .total_seconds = getJsonInteger(obj, "total_seconds") orelse return error.MissingProperty, + .remaining_seconds = getJsonInteger(obj, "remaining_seconds") orelse return error.MissingProperty, .expiration = (try dupeJsonString(allocator, obj, "expiration")) orelse return error.MissingProperty, .reason = Reason.parse(reason) orelse return error.UnknownReason }; @@ -281,21 +329,73 @@ pub const Cooldown = struct { pub const FightResult = struct { cooldown: Cooldown, + character: Character, fn parse(allocator: Allocator, obj: json.ObjectMap) !FightResult { const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty; + const character = getJsonObject(obj, "character") orelse return error.MissingProperty; return FightResult{ + .cooldown = try Cooldown.parse(allocator, cooldown), + .character = try Character.parse(allocator, character) + }; + } + + pub fn deinit(self: *FightResult) void { + self.cooldown.deinit(); + self.character.deinit(); + } +}; + +pub const MoveResult = struct { + cooldown: Cooldown, + + fn parse(allocator: Allocator, obj: json.ObjectMap) !MoveResult { + const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty; + + return MoveResult{ .cooldown = try Cooldown.parse(allocator, cooldown) }; } - pub fn deinit(self: FightResult) void { + pub fn deinit(self: MoveResult) void { self.cooldown.deinit(); } }; -const ArtifactsFetchResult = struct { +pub const GoldTransactionResult = struct { + cooldown: Cooldown, + + fn parse(allocator: Allocator, obj: json.ObjectMap) !GoldTransactionResult { + const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty; + + return GoldTransactionResult{ + .cooldown = try Cooldown.parse(allocator, cooldown) + }; + } + + pub fn deinit(self: GoldTransactionResult) void { + self.cooldown.deinit(); + } +}; + +pub const ItemTransactionResult = struct { + cooldown: Cooldown, + + fn parse(allocator: Allocator, obj: json.ObjectMap) !ItemTransactionResult { + const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty; + + return ItemTransactionResult{ + .cooldown = try Cooldown.parse(allocator, cooldown) + }; + } + + pub fn deinit(self: ItemTransactionResult) void { + self.cooldown.deinit(); + } +}; + +pub const ArtifactsFetchResult = struct { arena: std.heap.ArenaAllocator, status: std.http.Status, body: ?json.Value = null, @@ -325,7 +425,7 @@ pub fn init(allocator: Allocator) !ArtifactsAPI { }; } -fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8) !ArtifactsFetchResult { +fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload: ?[]const u8) !ArtifactsFetchResult { var uri = self.server_uri; uri.path = .{ .raw = path }; @@ -337,7 +437,8 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8) !Artifa var opts = std.http.Client.FetchOptions{ .method = method, .location = .{ .uri = uri }, - .response_storage = .{ .dynamic = &response_storage } + .payload = payload, + .response_storage = .{ .dynamic = &response_storage }, }; var authorization_header: ?[]u8 = null; @@ -349,16 +450,16 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8) !Artifa } const result = try self.client.fetch(opts); + const response_body = response_storage.items; if (result.status != .ok) { + std.log.debug("Request {} {s} failed with code {}: {s}", .{method, path, result.status, response_body}); return ArtifactsFetchResult{ .arena = arena, .status = result.status }; } - const response_body = response_storage.items; - const parsed = try json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }); if (parsed != json.Value.object) { return APIErrors.ParseFailed; @@ -371,6 +472,21 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8) !Artifa }; } +fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Method, path: []const u8, payload: ?[]const u8) !Result { + const result = try self.fetch(method, path, payload); + defer result.deinit(); + + if (result.status != .ok) { + return APIErrors.RequestFailed; + } + if (result.body == null) { + return APIErrors.ParseFailed; + } + + const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed; + return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed; +} + pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void { const url_dupe = self.allocator.dupe(u8, url); errdefer self.allocator.free(url_dupe); @@ -448,44 +564,31 @@ fn getJsonObject(object: json.ObjectMap, name: []const u8) ?json.ObjectMap { return asJsonObject(value.?); } +fn getJsonArray(object: json.ObjectMap, name: []const u8) ?json.Array { + const value = object.get(name); + if (value == null) { + return null; + } + + return asJsonArray(value.?); +} + pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus { - const result = try self.fetch(.GET, "/"); - defer result.deinit(); - - if (result.status != .ok) { - return APIErrors.RequestFailed; - } - if (result.body == null) { - return APIErrors.ParseFailed; - } - - const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed; - return ServerStatus.parse(self.allocator, body) catch return APIErrors.ParseFailed; + return try self.fetchAndParseObject(ServerStatus, .GET, "/", null); } pub fn getCharacter(self: *ArtifactsAPI, name: []const u8) !Character { const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name}); defer self.allocator.free(path); - const result = try self.fetch(.GET, path); - defer result.deinit(); - - if (result.status != .ok) { - return APIErrors.RequestFailed; - } - if (result.body == null) { - return APIErrors.ParseFailed; - } - - const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed; - return Character.parse(self.allocator, body) catch return APIErrors.ParseFailed; + return try self.fetchAndParseObject(Character, .GET, path, null); } pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList { const path = try std.fmt.allocPrint(self.allocator, "/my/characters", .{}); defer self.allocator.free(path); - const result = try self.fetch(.GET, path); + const result = try self.fetch(.GET, path, null); defer result.deinit(); if (result.status != .ok) { @@ -523,18 +626,37 @@ pub fn actionFight(self: *ArtifactsAPI, name: []const u8) !FightResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name}); defer self.allocator.free(path); - const result = try self.fetch(.POST, path); - defer result.deinit(); + return try self.fetchAndParseObject(FightResult, .POST, path, null); +} - if (result.status != .ok) { - return APIErrors.RequestFailed; - } - if (result.body == null) { - return APIErrors.ParseFailed; - } +pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveResult { + const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name}); + defer self.allocator.free(path); - const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed; - return FightResult.parse(self.allocator, body) catch return APIErrors.ParseFailed; + const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y}); + defer self.allocator.free(payload); + + return try self.fetchAndParseObject(MoveResult, .POST, path, payload); +} + +pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u64) !GoldTransactionResult { + const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit/gold", .{name}); + defer self.allocator.free(path); + + const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); + defer self.allocator.free(payload); + + return try self.fetchAndParseObject(GoldTransactionResult, .POST, path, payload); +} + +pub fn actionBankDeposit(self: *ArtifactsAPI, name: []const u8, code: []const u8, quantity: u64) !ItemTransactionResult { + const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit", .{name}); + defer self.allocator.free(path); + + const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); + defer self.allocator.free(payload); + + return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload); } pub fn deinit(self: *ArtifactsAPI) void { diff --git a/src/main.zig b/src/main.zig index d376917..c55e202 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,43 @@ const std = @import("std"); +const assert = std.debug.assert; const ArtifactsAPI = @import("artifacts.zig"); +fn waitForCooldown(remaining_seconds: i64) void { + if (remaining_seconds > 0) { + std.log.debug("Waiting for cooldown {}", .{remaining_seconds}); + std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(remaining_seconds)) + 1)); + } +} + +pub fn depositIfNeeded(api: *ArtifactsAPI, char: *ArtifactsAPI.Character) !void { + if (char.getItemCount() == char.inventory_max_items) return; + + { + const move_result = try api.actionMove(char.name, 4, 1); + defer move_result.deinit(); + waitForCooldown(move_result.cooldown.remaining_seconds); + } + + const deposit_gold = try api.actionBankDepositGold(char.name, @intCast(char.gold)); + defer deposit_gold.deinit(); + waitForCooldown(deposit_gold.cooldown.remaining_seconds); + + for (char.inventory.slots) |slot| { + if (slot.quantity == 0) continue; + + const deposit_item = try api.actionBankDeposit(char.name, slot.code, @intCast(slot.quantity)); + defer deposit_item.deinit(); + waitForCooldown(deposit_item.cooldown.remaining_seconds); + } + + { + const move_result = try api.actionMove(char.name, 0, 1); + defer move_result.deinit(); + waitForCooldown(move_result.cooldown.remaining_seconds); + } + +} + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -9,18 +46,75 @@ pub fn main() !void { var api = try ArtifactsAPI.init(allocator); defer api.deinit(); - { // Set auth token from environment variable - var env = try std.process.getEnvMap(allocator); - defer env.deinit(); + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); - const token = env.get("ARTIFACTS_TOKEN"); - try api.setToken(token); + if (args.len >= 2) { + const filename = args[1]; + const cwd = std.fs.cwd(); + + var token_buffer: [256]u8 = undefined; + const token = try cwd.readFile(filename, &token_buffer); + try api.setToken(std.mem.trim(u8,token,"\n\t ")); } - while (true) { - const result = try api.actionFight("Daisy"); - defer result.deinit(); + if (api.token == null) { + return error.MissingToken; + } - std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(result.cooldown.remaining_seconds)) + 1)); + const characters = try api.listMyCharacters(); + defer characters.deinit(); + + { + var longest_cooldown: i64 = 0; + + for (characters.items) |char| { + if (char.x == 0 and char.y == 1) continue; + + const result = try api.actionMove(char.name, 0, 1); + defer result.deinit(); + + longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds); + } + + if (longest_cooldown > 0) { + std.log.debug("Waiting for cooldown {}", .{longest_cooldown}); + + std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1)); + } + } + + std.log.info("Started main loop", .{}); + while (true) { + var results = std.ArrayList(ArtifactsAPI.FightResult).init(allocator); + defer { + for (results.items) |*result| { + result.deinit(); + } + results.deinit(); + } + + for (characters.items) |char| { + var result = try api.actionFight(char.name); + errdefer result.deinit(); + + try results.append(result); + } + + { + var longest_cooldown: i64 = 0; + for (results.items) |result| { + longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds); + } + + if (longest_cooldown > 0) { + std.log.debug("Waiting for cooldown {}", .{longest_cooldown}); + std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1)); + } + } + + for (results.items) |*result| { + try depositIfNeeded(&api, &result.character); + } } }