mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:35:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7695f48a | ||
|
|
0b9ebb04c8 | ||
|
|
10a3b3a046 | ||
|
|
7cf852ab52 | ||
|
|
2341df8998 | ||
|
|
190e71d3c4 | ||
|
|
c0a9b90715 | ||
|
|
6d13774fd0 | ||
|
|
41252ccaa6 | ||
|
|
ff05a35991 | ||
|
|
8b7e9eaeb6 | ||
|
|
7f80eb0ef2 | ||
|
|
ef151c2ef4 | ||
|
|
e7cad8dd36 | ||
|
|
c4edb2f6a0 | ||
|
|
11709e6d48 | ||
|
|
0b0fcc411d | ||
|
|
1aadcc863f | ||
|
|
9ac167e66c | ||
|
|
1530873df5 | ||
|
|
75a5b4fa21 | ||
|
|
b6c45da7bc | ||
|
|
5e30673281 | ||
|
|
d38c611a18 | ||
|
|
570bc557f5 | ||
|
|
c1f4152cae | ||
|
|
0472362abc | ||
|
|
b732b60520 | ||
|
|
fc1f988654 | ||
|
|
7ef93c4a11 | ||
|
|
247aa3e685 | ||
|
|
aa0292bad6 | ||
|
|
08cd608ac2 | ||
|
|
f85a32dc08 | ||
|
|
b5deb69ef7 | ||
|
|
872883f710 | ||
|
|
3d402e7bdd | ||
|
|
862480c470 | ||
|
|
98b5d7f034 | ||
|
|
e5c5a929ec | ||
|
|
d39ee392b9 | ||
|
|
b574c6c950 | ||
|
|
074c984f3b | ||
|
|
2817ca529d | ||
|
|
4f932d0a56 | ||
|
|
2dd931dc8f | ||
|
|
c7ab2ecb44 | ||
|
|
8466ab6ed9 | ||
|
|
7b74905973 | ||
|
|
ecfb5a0a94 | ||
|
|
967cb74aa5 | ||
|
|
34daf8f38d | ||
|
|
e12359821c | ||
|
|
eea41e6c05 | ||
|
|
36d1e75719 | ||
|
|
35e93c8ce1 | ||
|
|
d603b01733 | ||
|
|
6c66b5df82 | ||
|
|
3b59e21407 | ||
|
|
d0e74ac4cb | ||
|
|
c04aa3d753 | ||
|
|
f3fade7ed2 | ||
|
|
79d8e837e8 | ||
|
|
c39795fa9f | ||
|
|
4a7c55a382 | ||
|
|
7e4396552e | ||
|
|
40219f2a67 | ||
|
|
ce35c176ee | ||
|
|
a852f03214 | ||
|
|
7c8b43d91b | ||
|
|
ecf763b24b | ||
|
|
8ad4fc52e3 | ||
|
|
7e48f6fd2b | ||
|
|
5c81832cde | ||
|
|
569458d691 | ||
|
|
c825bf4606 |
@@ -37,6 +37,8 @@ build:ubsan --strip=never
|
||||
build:ubsan --copt=-fno-omit-frame-pointer
|
||||
build:ubsan --copt=-Wno-macro-redefined
|
||||
build:ubsan --copt=-fsanitize=undefined
|
||||
# Protobuf's arena implementation trips UBSan alignment checks in external code on macOS.
|
||||
build:ubsan --copt=-fno-sanitize=alignment
|
||||
build:ubsan --linkopt=-fsanitize=undefined
|
||||
test:ubsan --test_env=UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Shardok Sanitizer Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "src/main/cpp/net/eagle0/shardok/**"
|
||||
- "src/test/cpp/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "WORKSPACE"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- "BUILD.bazel"
|
||||
- ".bazelrc"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- ".github/workflows/shardok_sanitizer_test.yml"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- "ci/github_actions/summarize_bazel_bep.py"
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/main/cpp/net/eagle0/shardok/**"
|
||||
- "src/test/cpp/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "WORKSPACE"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- "BUILD.bazel"
|
||||
- ".bazelrc"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- ".github/workflows/shardok_sanitizer_test.yml"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- "ci/github_actions/summarize_bazel_bep.py"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
shardok-sanitizer-test:
|
||||
name: Shardok ${{ matrix.name }}
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: ASan
|
||||
config: asan
|
||||
- name: UBSan
|
||||
config: ubsan
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Run Shardok sanitizer tests
|
||||
run: bazel test --config=${{ matrix.config }} --build_event_json_file=${{ matrix.config }}.json //src/test/cpp/net/eagle0/shardok/...
|
||||
- name: Summarize Bazel build metrics
|
||||
if: always()
|
||||
run: python3 ci/github_actions/summarize_bazel_bep.py ${{ matrix.config }}.json
|
||||
- name: Archive sanitizer test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shardok-${{ matrix.config }}.json
|
||||
path: ${{ matrix.config }}.json
|
||||
retention-days: 3
|
||||
@@ -28,7 +28,7 @@ static inline auto HashBuffer(const uint8_t* data, size_t size) -> uint64_t {
|
||||
|
||||
// Process 8 bytes at a time
|
||||
while (data + 8 <= end) {
|
||||
uint64_t word;
|
||||
uint64_t word = 0;
|
||||
// Use memcpy to avoid alignment issues and let compiler optimize
|
||||
__builtin_memcpy(&word, data, sizeof(word));
|
||||
hash ^= word;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
|
||||
#define ITERABLE_BITSET_INDEX_CHECKS false
|
||||
|
||||
@@ -104,9 +105,11 @@ public:
|
||||
|
||||
class Iter {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = size_t;
|
||||
using pointer = size_t*;
|
||||
using reference = size_t&;
|
||||
using pointer = void;
|
||||
using reference = size_t;
|
||||
|
||||
private:
|
||||
size_t storeIndex;
|
||||
@@ -142,13 +145,20 @@ public:
|
||||
auto operator!=(const Iter& rhs) const -> bool {
|
||||
return storeIndex != rhs.storeIndex || bitIndex != rhs.bitIndex;
|
||||
}
|
||||
void operator++() {
|
||||
auto operator++() -> Iter& {
|
||||
bitIndex++;
|
||||
if (bitIndex >= STORE_BITS) {
|
||||
storeIndex++;
|
||||
bitIndex = 0;
|
||||
}
|
||||
ToNext();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator++(int) -> Iter {
|
||||
Iter previous = *this;
|
||||
++(*this);
|
||||
return previous;
|
||||
}
|
||||
|
||||
auto operator*() const -> size_t { return (storeIndex << BIT_SHIFT) + bitIndex; }
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#include "RandomGenerator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <stdexcept>
|
||||
|
||||
constexpr double kOpenEndedHighThreshold = 95.0;
|
||||
constexpr double kOpenEndedLowThreshold = 5.0;
|
||||
@@ -67,7 +69,8 @@ auto RandomGenerator::ReverseOpenEndedPercentile() -> double {
|
||||
}
|
||||
|
||||
auto RandomGenerator::IntBetween(const int min, const int max) -> int {
|
||||
return int(DoubleZeroToOne() * (max - min)) + min;
|
||||
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
|
||||
return std::min(int(DoubleZeroToOne() * (max - min)) + min, max - 1);
|
||||
}
|
||||
|
||||
auto RandomGenerator::IntBelow(const int max) -> int { return IntBetween(0, max); }
|
||||
@@ -94,6 +97,7 @@ StdLibraryGenerator::StdLibraryGenerator(const std::mt19937_64::result_type seed
|
||||
}
|
||||
|
||||
auto StdLibraryGenerator::IntBetween(const int min, const int max) -> int {
|
||||
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
|
||||
std::uniform_int_distribution<int> unifInt(min, max - 1);
|
||||
return unifInt(engine);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
class RandomGenerator;
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
|
||||
template<class T>
|
||||
auto RandomElement(const std::vector<T>& vec) -> T {
|
||||
if (vec.empty()) { throw std::invalid_argument("cannot choose from an empty vector"); }
|
||||
return vec.at(static_cast<std::size_t>(IntBelow(static_cast<int>(vec.size()))));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#ifndef EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
#define EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -36,7 +38,17 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {}
|
||||
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {
|
||||
if (sequence.empty()) {
|
||||
throw std::invalid_argument("sequence random generator requires at least one value");
|
||||
}
|
||||
for (const double value : sequence) {
|
||||
if (!std::isfinite(value) || value < 0.0 || value > 1.0) {
|
||||
throw std::invalid_argument(
|
||||
"sequence random generator values must be between 0 and 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
static auto WithSequence(const std::vector<double>& s)
|
||||
-> std::shared_ptr<SequenceRandomGenerator> {
|
||||
return std::make_shared<SequenceRandomGenerator>(s);
|
||||
|
||||
@@ -9,18 +9,24 @@
|
||||
#ifndef EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
#define EAGLE0_COMMON_BYTE_VECTOR_HPP
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <initializer_list>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using std::string;
|
||||
using std::vector;
|
||||
|
||||
class byte_vector : public vector<uint8_t> {
|
||||
class byte_vector {
|
||||
private:
|
||||
std::vector<uint8_t> bytes_;
|
||||
|
||||
public:
|
||||
class HexException : public std::exception {
|
||||
const std::string description;
|
||||
@@ -35,60 +41,78 @@ public:
|
||||
};
|
||||
|
||||
private:
|
||||
static auto HexNibbleToValue(const char nibble) -> uint8_t {
|
||||
if (nibble >= '0' && nibble <= '9') {
|
||||
return uint8_t(nibble - '0');
|
||||
} else if (nibble >= 'A' && nibble <= 'F') {
|
||||
return uint8_t(nibble - 'A' + 10);
|
||||
} else if (nibble >= 'a' && nibble <= 'f') {
|
||||
return uint8_t(nibble - 'a' + 10);
|
||||
static auto HexCharsToByte(const std::string_view digits) -> uint8_t {
|
||||
unsigned int parsedByte = 0;
|
||||
const auto [ptr, ec] =
|
||||
std::from_chars(digits.data(), digits.data() + digits.size(), parsedByte, 16);
|
||||
if (ec != std::errc() || ptr != digits.data() + digits.size()) {
|
||||
throw HexException(ptr == digits.data() + digits.size() ? digits.back() : *ptr);
|
||||
}
|
||||
throw HexException(nibble);
|
||||
}
|
||||
|
||||
static auto HexCharsToByte(std::istringstream& str) -> uint8_t {
|
||||
uint8_t b = 0;
|
||||
char c = 0;
|
||||
str >> c;
|
||||
b |= (HexNibbleToValue(c) << 4u);
|
||||
str >> c;
|
||||
b |= HexNibbleToValue(c);
|
||||
return b;
|
||||
return static_cast<uint8_t>(parsedByte);
|
||||
}
|
||||
|
||||
public:
|
||||
byte_vector() = default;
|
||||
byte_vector(const std::initializer_list<uint8_t> list) : vector<uint8_t>(list) {}
|
||||
byte_vector(const std::initializer_list<uint8_t> list) : bytes_(list) {}
|
||||
template<class InputIt>
|
||||
byte_vector(InputIt begin, InputIt end) : vector<uint8_t>(begin, end) {}
|
||||
byte_vector(InputIt begin, InputIt end) : bytes_(begin, end) {}
|
||||
byte_vector(const byte_vector& rhs) = default;
|
||||
explicit byte_vector(const std::string& s) : byte_vector(s.begin(), s.end()) {}
|
||||
explicit byte_vector(const size_t size) : vector<uint8_t>(size) {}
|
||||
explicit byte_vector(void* data, const size_t size) : vector<uint8_t>(size) {
|
||||
this->assign((uint8_t*)data, (uint8_t*)data + size);
|
||||
byte_vector(byte_vector&& rhs) noexcept = default;
|
||||
explicit byte_vector(const std::string& s) : bytes_(s.begin(), s.end()) {}
|
||||
explicit byte_vector(std::vector<uint8_t> bytes) : bytes_(std::move(bytes)) {}
|
||||
explicit byte_vector(const size_t size) : bytes_(size) {}
|
||||
explicit byte_vector(const void* data, const size_t size) {
|
||||
auto* bytes = static_cast<const uint8_t*>(data);
|
||||
bytes_.assign(bytes, bytes + size);
|
||||
}
|
||||
|
||||
auto operator==(const vector<uint8_t>& vec) -> bool {
|
||||
if (this->size() != vec.size()) return false;
|
||||
for (size_t i = 0; i < size(); i++) {
|
||||
if ((*this)[i] != vec[i]) return false;
|
||||
}
|
||||
return true;
|
||||
auto operator=(const byte_vector& rhs) -> byte_vector& = default;
|
||||
auto operator=(byte_vector&& rhs) noexcept -> byte_vector& = default;
|
||||
|
||||
[[nodiscard]] auto operator==(const std::vector<uint8_t>& vec) const -> bool {
|
||||
return bytes_ == vec;
|
||||
}
|
||||
[[nodiscard]] auto operator==(const byte_vector& vec) const -> bool {
|
||||
return bytes_ == vec.bytes_;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto data() -> uint8_t* { return bytes_.data(); }
|
||||
[[nodiscard]] auto data() const -> const uint8_t* { return bytes_.data(); }
|
||||
[[nodiscard]] auto size() const -> size_t { return bytes_.size(); }
|
||||
[[nodiscard]] auto empty() const -> bool { return bytes_.empty(); }
|
||||
void reserve(const size_t count) { bytes_.reserve(count); }
|
||||
void resize(const size_t count) { bytes_.resize(count); }
|
||||
void swap(byte_vector& other) noexcept { bytes_.swap(other.bytes_); }
|
||||
|
||||
[[nodiscard]] auto begin() -> std::vector<uint8_t>::iterator { return bytes_.begin(); }
|
||||
[[nodiscard]] auto begin() const -> std::vector<uint8_t>::const_iterator {
|
||||
return bytes_.begin();
|
||||
}
|
||||
[[nodiscard]] auto end() -> std::vector<uint8_t>::iterator { return bytes_.end(); }
|
||||
[[nodiscard]] auto end() const -> std::vector<uint8_t>::const_iterator { return bytes_.end(); }
|
||||
[[nodiscard]] auto cbegin() const -> std::vector<uint8_t>::const_iterator {
|
||||
return bytes_.cbegin();
|
||||
}
|
||||
[[nodiscard]] auto cend() const -> std::vector<uint8_t>::const_iterator {
|
||||
return bytes_.cend();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto operator[](const size_t index) -> uint8_t& { return bytes_[index]; }
|
||||
[[nodiscard]] auto operator[](const size_t index) const -> uint8_t { return bytes_[index]; }
|
||||
|
||||
auto append(const char* bytes, const size_t length) {
|
||||
const auto previousSize = this->size();
|
||||
this->resize(previousSize + length);
|
||||
std::memcpy(this->data() + previousSize, bytes, length);
|
||||
const auto previousSize = size();
|
||||
resize(previousSize + length);
|
||||
std::memcpy(data() + previousSize, bytes, length);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
auto append(const T* elts, const size_t count) {
|
||||
append((char*)elts, count * sizeof(T));
|
||||
append(reinterpret_cast<const char*>(elts), count * sizeof(T));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
auto append(const vector<T> elts) {
|
||||
auto append(const vector<T>& elts) {
|
||||
append(elts.data(), elts.size());
|
||||
}
|
||||
|
||||
@@ -98,7 +122,7 @@ public:
|
||||
}
|
||||
|
||||
auto operator+=(const uint8_t c) -> byte_vector& {
|
||||
this->insert(end(), c);
|
||||
bytes_.push_back(c);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -111,7 +135,7 @@ public:
|
||||
|
||||
template<class Iterable>
|
||||
auto operator+=(const Iterable& rhs) -> byte_vector& {
|
||||
this->insert(end(), rhs.cbegin(), rhs.cend());
|
||||
bytes_.insert(end(), rhs.cbegin(), rhs.cend());
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -119,7 +143,7 @@ public:
|
||||
if (size() < 8) {
|
||||
uint64_t total = 0;
|
||||
for (size_t i = 0; i < size(); i++) {
|
||||
total <<= 8u;
|
||||
total <<= 8U;
|
||||
total += (*this)[i];
|
||||
}
|
||||
return total;
|
||||
@@ -128,9 +152,9 @@ public:
|
||||
uint64_t firstBytes = 0;
|
||||
uint64_t lastBytes = 0;
|
||||
for (unsigned i = 0; i < 8; i++) {
|
||||
firstBytes <<= 8u;
|
||||
firstBytes <<= 8U;
|
||||
firstBytes += (*this)[i];
|
||||
lastBytes <<= 8u;
|
||||
lastBytes <<= 8U;
|
||||
lastBytes += (*this)[i];
|
||||
}
|
||||
|
||||
@@ -141,8 +165,8 @@ public:
|
||||
std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
|
||||
const std::streamsize size = inputFileStream.tellg();
|
||||
inputFileStream.seekg(0, std::ios::beg);
|
||||
this->resize((size_t(size)));
|
||||
inputFileStream.read((char*)this->data(), size);
|
||||
this->resize(static_cast<size_t>(size));
|
||||
inputFileStream.read(reinterpret_cast<char*>(this->data()), size);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -153,34 +177,41 @@ public:
|
||||
}
|
||||
const std::streamsize size = inputFileStream.tellg();
|
||||
inputFileStream.seekg(0, std::ios::beg);
|
||||
byte_vector serializedGame((size_t(size)));
|
||||
inputFileStream.read((char*)serializedGame.data(), size);
|
||||
byte_vector serializedGame(static_cast<size_t>(size));
|
||||
inputFileStream.read(reinterpret_cast<char*>(serializedGame.data()), size);
|
||||
return serializedGame;
|
||||
}
|
||||
|
||||
static auto FromHex(const char* hexString) -> byte_vector { return FromHex(string(hexString)); }
|
||||
|
||||
static auto FromHex(string hexString) -> byte_vector {
|
||||
if (hexString[0] == '0' && (hexString[1] == 'x' || hexString[1] == 'X')) {
|
||||
hexString = string(hexString.begin() + 1, hexString.end());
|
||||
static auto FromHex(const string& hexString) -> byte_vector {
|
||||
std::string_view digits(hexString);
|
||||
if (digits.starts_with("0x") || digits.starts_with("0X")) {
|
||||
digits.remove_prefix(2);
|
||||
} else if (digits.starts_with("x") || digits.starts_with("X")) {
|
||||
digits.remove_prefix(1);
|
||||
}
|
||||
if (hexString[0] == 'x' || hexString[0] == 'X') {
|
||||
hexString = string(hexString.begin() + 1, hexString.end());
|
||||
}
|
||||
byte_vector vec;
|
||||
const size_t len = hexString.size();
|
||||
vec.reserve(len / 2);
|
||||
std::istringstream hexStream(hexString);
|
||||
|
||||
while (size_t(hexStream.tellg()) < len) { vec += HexCharsToByte(hexStream); }
|
||||
if (digits.size() % 2 != 0) { throw HexException(digits.empty() ? '\0' : digits.back()); }
|
||||
|
||||
byte_vector vec;
|
||||
vec.reserve(digits.size() / 2);
|
||||
|
||||
for (size_t index = 0; index < digits.size(); index += 2) {
|
||||
vec += HexCharsToByte(digits.substr(index, 2));
|
||||
}
|
||||
return vec;
|
||||
}
|
||||
|
||||
explicit operator std::string() const { return std::string(begin(), end()); }
|
||||
};
|
||||
|
||||
inline auto operator<<(std::ostream& ostr, byte_vector vec) -> std::ostream& {
|
||||
ostr.write((char*)vec.data(), (std::streamsize)vec.size());
|
||||
inline auto operator==(const std::vector<uint8_t>& lhs, const byte_vector& rhs) -> bool {
|
||||
return rhs == lhs;
|
||||
}
|
||||
|
||||
inline auto operator<<(std::ostream& ostr, const byte_vector& vec) -> std::ostream& {
|
||||
ostr.write(reinterpret_cast<const char*>(vec.data()), static_cast<std::streamsize>(vec.size()));
|
||||
return ostr;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ struct alignas(64) OneMapInfo {
|
||||
std::map<int, int> positionsRequiringCrossing;
|
||||
};
|
||||
|
||||
auto CalculateMap(
|
||||
[[nodiscard]] auto CalculateMap(
|
||||
const std::string& mapName,
|
||||
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
|
||||
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo;
|
||||
|
||||
@@ -25,25 +25,44 @@ DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
|
||||
|
||||
struct TargetAndDistance {
|
||||
Coords target;
|
||||
size_t targetIndex;
|
||||
CoordsSet attackLocations;
|
||||
double targetPower;
|
||||
DIST_T distance;
|
||||
|
||||
TargetAndDistance(const Coords& t, const CoordsSet& al, const double tp, const DIST_T d)
|
||||
TargetAndDistance(
|
||||
const Coords& t,
|
||||
const size_t ti,
|
||||
const CoordsSet& al,
|
||||
const double tp,
|
||||
const DIST_T d)
|
||||
: target(t),
|
||||
targetIndex(ti),
|
||||
attackLocations(al),
|
||||
targetPower(tp),
|
||||
distance(d) {}
|
||||
};
|
||||
|
||||
auto CoordsIndex(const Coords& coords, const int columnCount) {
|
||||
return coords.row() * columnCount + coords.column();
|
||||
}
|
||||
|
||||
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
|
||||
return Coords(
|
||||
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
|
||||
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
|
||||
}
|
||||
|
||||
auto MinDistance(
|
||||
const Coords& start,
|
||||
const CoordsSet& destinations,
|
||||
const ActionPointDistances* apd) -> DIST_T {
|
||||
DIST_T minDistance = ActionPointDistances::IMPOSSIBLE;
|
||||
const int startIndex = CoordsIndex(start, destinations.ColumnCount());
|
||||
|
||||
for (const Coords& dest : destinations) {
|
||||
if (const DIST_T distance = apd->Distance(start, dest); distance < minDistance) {
|
||||
for (const auto destIndex : destinations.indexIterator()) {
|
||||
if (const DIST_T distance = apd->Distance(startIndex, static_cast<int>(destIndex));
|
||||
distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
@@ -113,10 +132,6 @@ auto EffectiveDistance(
|
||||
|
||||
auto Power(const Unit* unit) -> double { return unit->battalion().size(); }
|
||||
|
||||
auto CoordsIndex(const Coords& coords, const int columnCount) {
|
||||
return coords.row() * columnCount + coords.column();
|
||||
}
|
||||
|
||||
// Chooses a list of targets in priority order for each unit.
|
||||
auto GenerateTargetPriorities(
|
||||
const vector<const Unit*>& occupants,
|
||||
@@ -163,10 +178,10 @@ auto GenerateTargetPriorities(
|
||||
bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
|
||||
}
|
||||
|
||||
for (const Coords& targetLocation : targets) {
|
||||
const auto coordsIndex = CoordsIndex(targetLocation, cc);
|
||||
const auto& occupant = occupants[coordsIndex];
|
||||
for (const auto targetIndex : targets.indexIterator()) {
|
||||
const auto& occupant = occupants[targetIndex];
|
||||
|
||||
const Coords targetLocation = CoordsFromIndex(targetIndex, cc);
|
||||
const auto& attackLocations = alCache->CachedLocations(targetLocation, isLateGame);
|
||||
|
||||
if (occupant && occupant->player_id() != attackerId) {
|
||||
@@ -179,12 +194,14 @@ auto GenerateTargetPriorities(
|
||||
|
||||
targetsWithDistance.emplace_back(
|
||||
targetLocation,
|
||||
targetIndex,
|
||||
attackLocsForUnit,
|
||||
occupantPower,
|
||||
distance);
|
||||
} else {
|
||||
targetsWithDistance.emplace_back(
|
||||
targetLocation,
|
||||
targetIndex,
|
||||
attackLocations.LocationsWithEnemyInRange(unit),
|
||||
occupantPower,
|
||||
ActionPointDistances::IMPOSSIBLE);
|
||||
@@ -195,13 +212,11 @@ auto GenerateTargetPriorities(
|
||||
// Sort by distance
|
||||
std::ranges::sort(
|
||||
targetsWithDistance,
|
||||
[&powerAttackingEachTarget,
|
||||
cc](const TargetAndDistance& left, const TargetAndDistance& right) {
|
||||
const auto leftIndex = CoordsIndex(left.target, cc);
|
||||
const auto rightIndex = CoordsIndex(right.target, cc);
|
||||
|
||||
const double powerAttackingLeft = powerAttackingEachTarget[leftIndex];
|
||||
const double powerAttackingRight = powerAttackingEachTarget[rightIndex];
|
||||
[&powerAttackingEachTarget](
|
||||
const TargetAndDistance& left,
|
||||
const TargetAndDistance& right) {
|
||||
const double powerAttackingLeft = powerAttackingEachTarget[left.targetIndex];
|
||||
const double powerAttackingRight = powerAttackingEachTarget[right.targetIndex];
|
||||
|
||||
const bool leftAlreadyOverwhelmed =
|
||||
powerAttackingLeft >= kOverpowerRatio * left.targetPower;
|
||||
@@ -215,8 +230,7 @@ auto GenerateTargetPriorities(
|
||||
});
|
||||
|
||||
if (!targetsWithDistance.empty()) {
|
||||
powerAttackingEachTarget[CoordsIndex(targetsWithDistance.front().target, cc)] +=
|
||||
Power(unit);
|
||||
powerAttackingEachTarget[targetsWithDistance.front().targetIndex] += Power(unit);
|
||||
}
|
||||
|
||||
tpl.priorityOrder.reserve(targetsWithDistance.size());
|
||||
|
||||
@@ -114,8 +114,7 @@ auto AttackLocationsCache::CachedLocations(const CoordsSet &targets, const bool
|
||||
-> AttackLocations {
|
||||
AttackLocations al(rowCount, columnCount);
|
||||
|
||||
for (const auto &target : targets) {
|
||||
const int index = target.row() * columnCount + target.column();
|
||||
for (const auto index : targets.indexIterator()) {
|
||||
if (lateGame) {
|
||||
al += lateGameLocations[index];
|
||||
} else {
|
||||
@@ -137,4 +136,4 @@ auto AttackLocationsCache::CachedLocations(const vector<const Unit *> &enemies,
|
||||
return CachedLocations(targets, lateGame);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -116,8 +116,9 @@ auto DefenderDistanceBuf(
|
||||
double sum = 0.0;
|
||||
double decr = kPerUnitDebufDecay;
|
||||
for (const WithoutAndWith &pointCost : pointCosts) {
|
||||
const int effectiveCost =
|
||||
(pointCost.without == 999) ? int(1.2 * pointCost.with) : pointCost.without;
|
||||
const int effectiveCost = (pointCost.without == 999)
|
||||
? static_cast<int>(1.2 * pointCost.with)
|
||||
: pointCost.without;
|
||||
sum += decr / (effectiveCost + 1);
|
||||
decr *= kPerUnitDebufDecay;
|
||||
}
|
||||
|
||||
@@ -5,15 +5,34 @@
|
||||
#include "AIMinimumDistanceAndTarget.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
auto CoordsIndex(const Coords &coords, const int columnCount) -> int {
|
||||
return coords.row() * columnCount + coords.column();
|
||||
}
|
||||
|
||||
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
|
||||
return Coords(
|
||||
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
|
||||
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto MinimumDistanceAndTarget(
|
||||
const ActionPointDistances *apd,
|
||||
const Coords &origin,
|
||||
const CoordsSet &destinations) -> CoordsAndDistance {
|
||||
CoordsAndDistance min{Coords(-1, -1), ActionPointDistances::IMPOSSIBLE};
|
||||
for (const auto &dest : destinations) {
|
||||
const DIST_T dist = apd->Distance(origin, dest);
|
||||
const int columnCount = destinations.ColumnCount();
|
||||
const int originIndex = CoordsIndex(origin, columnCount);
|
||||
|
||||
for (const auto destIndex : destinations.indexIterator()) {
|
||||
const DIST_T dist = apd->Distance(originIndex, static_cast<int>(destIndex));
|
||||
if (dist == ActionPointDistances::IMPOSSIBLE) continue;
|
||||
if (dist < min.distance) min = CoordsAndDistance{dest, dist};
|
||||
if (dist < min.distance) {
|
||||
min = CoordsAndDistance{CoordsFromIndex(destIndex, columnCount), dist};
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
@@ -25,4 +44,4 @@ auto MinimumDistance(
|
||||
const CoordsSet &destinations) -> int {
|
||||
return MinimumDistanceAndTarget(apd, origin, destinations).distance;
|
||||
}
|
||||
} // namespace shardok
|
||||
} // namespace shardok
|
||||
|
||||
@@ -25,7 +25,8 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
|
||||
|
||||
[[nodiscard]] auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
|
||||
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
|
||||
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &gameState, PlayerId pid)
|
||||
-> const PlayerInfo *;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -88,8 +88,8 @@ auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
|
||||
break;
|
||||
}
|
||||
|
||||
const double battalionValue = battalionTypeMultiplier * (1.0 + armament / 100.0) *
|
||||
(1.0 + training / 100.0) * (0.5 + morale / 100.0) *
|
||||
const double battalionValue = battalionTypeMultiplier * (1.0 + (armament / 100.0)) *
|
||||
(1.0 + (training / 100.0)) * (0.5 + (morale / 100.0)) *
|
||||
unit->battalion().size();
|
||||
|
||||
const double heroValue =
|
||||
@@ -120,15 +120,16 @@ auto archeryValue(const vector<const Unit *> &enemyUnits, const bool canShootNow
|
||||
|
||||
auto reduceValue(const Unit *unit, const Terrain *unitTerrain) -> double {
|
||||
// TODO: make this depend on value of reducing castle too
|
||||
if (unit->fortified() || unitTerrain->modifier().castle().present())
|
||||
if (unit->fortified() || unitTerrain->modifier().castle().present()) {
|
||||
return kReduceFortifiedPossibleValue;
|
||||
else if (AllowsFortification(unitTerrain))
|
||||
return kReduceUnfortifiedPossibleValue;
|
||||
else
|
||||
return 0.0;
|
||||
}
|
||||
if (AllowsFortification(unitTerrain)) { return kReduceUnfortifiedPossibleValue; }
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
auto fearValue(const vector<const Unit *> &) -> double { return kFearPossibleValue; }
|
||||
auto fearValue([[maybe_unused]] const vector<const Unit *> &enemyUnits) -> double {
|
||||
return kFearPossibleValue;
|
||||
}
|
||||
|
||||
auto lightningValue(const Unit *unit) -> double {
|
||||
// TODO: make this depend on the value of the targets
|
||||
@@ -235,9 +236,8 @@ auto meteorValue(
|
||||
multiplier = kMeteorNoneRoundMultiplier * vigorFactor;
|
||||
}
|
||||
return maxValue * multiplier;
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
case net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET:
|
||||
@@ -247,9 +247,8 @@ auto meteorValue(
|
||||
map,
|
||||
enemyOccupants,
|
||||
friendlyOccupants);
|
||||
} else {
|
||||
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
|
||||
}
|
||||
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
|
||||
case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
|
||||
return kMeteorCastRoundMultiplier *
|
||||
meteorDropRawValue(
|
||||
|
||||
@@ -169,6 +169,7 @@ auto WaterCrossingTiles(
|
||||
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
|
||||
mapCopy->mutable_terrain()->GetMutableObject(index));
|
||||
terr->mutable_modifier().mutable_bridge().mutate_present(true);
|
||||
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
|
||||
terr->mutable_modifier().mutable_fire().mutate_present(false);
|
||||
|
||||
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
|
||||
|
||||
@@ -133,8 +133,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
|
||||
const auto cc = gameState->hex_map()->column_count();
|
||||
const auto occupants = Occupants(*gameState->units(), rc, cc);
|
||||
|
||||
for (const Coords& criticalTileLocation : criticalTileLocations) {
|
||||
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
|
||||
for (const auto index : criticalTileLocations.indexIterator()) {
|
||||
const auto& occupant = occupants[index];
|
||||
|
||||
if (occupant && occupant->battalion().type() !=
|
||||
|
||||
+5
-5
@@ -32,7 +32,7 @@ public:
|
||||
* Initialize game settings from the default configuration files.
|
||||
* Must be called before creating game states.
|
||||
*/
|
||||
static auto InitializeGameSettings() -> GameSettingsSPtr;
|
||||
[[nodiscard]] static auto InitializeGameSettings() -> GameSettingsSPtr;
|
||||
|
||||
/**
|
||||
* Create the standard "Perf" test configuration:
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
* @param defenderToggle If true, AI is defender; if false, AI is attacker
|
||||
* @return A GameStateW with the configured battle
|
||||
*/
|
||||
static auto CreatePerfTestGameState(
|
||||
[[nodiscard]] static auto CreatePerfTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
bool defenderToggle = false) -> GameStateW;
|
||||
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
* @param defenderToggle If true, AI is defender; if false, AI is attacker
|
||||
* @return A GameStateW with the configured battle
|
||||
*/
|
||||
static auto CreateCustomTestGameState(
|
||||
[[nodiscard]] static auto CreateCustomTestGameState(
|
||||
const GameSettingsSPtr& settings,
|
||||
const std::string& mapName,
|
||||
int aiUnitCount,
|
||||
@@ -67,11 +67,11 @@ public:
|
||||
|
||||
private:
|
||||
// Helper functions for building game state components
|
||||
static auto
|
||||
[[nodiscard]] static auto
|
||||
AddPlayerInfo(flatbuffers::FlatBufferBuilder& fbb, int playerId, bool isDefender, int food)
|
||||
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
|
||||
|
||||
static auto AddGenericUnit(
|
||||
[[nodiscard]] static auto AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords& location,
|
||||
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
* @param mctsConfig MCTS settings, used only when algorithmType is MCTS
|
||||
* @return Unique pointer to created ShardokAIClient
|
||||
*/
|
||||
static auto Create(
|
||||
[[nodiscard]] static auto Create(
|
||||
PlayerId playerId,
|
||||
bool isDefender,
|
||||
const net::eagle0::shardok::storage::fb::HexMap* hexMap,
|
||||
|
||||
@@ -29,7 +29,7 @@ public:
|
||||
*
|
||||
* @return Shared pointer to initialized GameSettings
|
||||
*/
|
||||
static auto CreateDefault() -> GameSettingsSPtr;
|
||||
[[nodiscard]] static auto CreateDefault() -> GameSettingsSPtr;
|
||||
};
|
||||
|
||||
} // namespace shardok::ai_testing_common
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -27,6 +28,38 @@ using net::eagle0::common::ScopedShardokLatencyTrace;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] auto DescribeCommandForLog(const CommandSPtr &command) -> std::string {
|
||||
if (!command) { return "<null command>"; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "type=" << command->GetCommandType()
|
||||
<< " player=" << static_cast<int>(command->GetPlayerId())
|
||||
<< " actor=" << command->GetActorUnitId() << " target=("
|
||||
<< static_cast<int>(command->GetTargetRow()) << ','
|
||||
<< static_cast<int>(command->GetTargetColumn()) << ')';
|
||||
|
||||
if (command->HasOdds()) { out << " odds_percentile=" << command->GetOddsPercentile(); }
|
||||
|
||||
try {
|
||||
out << " proto={" << command->GetCommandProto().ShortDebugString() << '}';
|
||||
} catch (const std::exception &e) { out << " proto_error={" << e.what() << '}'; }
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto DescribeCommandsForLog(const CommandList &commands) -> std::string {
|
||||
std::ostringstream out;
|
||||
out << "available_command_count=" << commands.size();
|
||||
for (size_t i = 0; i < commands.size(); ++i) {
|
||||
out << "\n [" << i << "] " << DescribeCommandForLog(commands[i]);
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class InvalidTokenException : public std::exception {};
|
||||
|
||||
class MismatchedPlayerIdException : public std::exception {
|
||||
@@ -154,6 +187,8 @@ void ShardokGameController::DoAIThread() {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string aiPhase = "choosing command";
|
||||
int64_t chosenIndex = -1;
|
||||
try {
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
auto results = [&] {
|
||||
@@ -165,6 +200,7 @@ void ShardokGameController::DoAIThread() {
|
||||
{"available_command_count", std::to_string(availableCommands->size())}});
|
||||
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
}();
|
||||
chosenIndex = results.chosenIndex;
|
||||
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
@@ -183,6 +219,7 @@ void ShardokGameController::DoAIThread() {
|
||||
}
|
||||
|
||||
{
|
||||
aiPhase = "posting chosen command";
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"ai_engine_post_command",
|
||||
cachedGameId,
|
||||
@@ -193,6 +230,7 @@ void ShardokGameController::DoAIThread() {
|
||||
}
|
||||
LockedNotifyClients();
|
||||
{
|
||||
aiPhase = "posting forced follow-up commands";
|
||||
ScopedShardokLatencyTrace trace(
|
||||
"ai_engine_post_forced_commands",
|
||||
cachedGameId,
|
||||
@@ -206,6 +244,17 @@ void ShardokGameController::DoAIThread() {
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "AI thread exception in game " << cachedGameId << ", player " << playerId
|
||||
<< ", round " << gsv.current_round() << ": " << e.what() << '\n';
|
||||
std::cerr << "AI failure context: phase=" << aiPhase
|
||||
<< ", expected_history_count=" << expectedHistoryCount
|
||||
<< ", current_history_count=" << engine->GetUnfilteredHistoryCount()
|
||||
<< ", chosen_index=" << chosenIndex
|
||||
<< ", current_player=" << static_cast<int>(engine->GetCurrentPlayerId())
|
||||
<< '\n';
|
||||
if (availableCommands) {
|
||||
std::cerr << DescribeCommandsForLog(*availableCommands) << '\n';
|
||||
} else {
|
||||
std::cerr << "available_command_count=<cleared>" << '\n';
|
||||
}
|
||||
|
||||
void *backtraceArray[20];
|
||||
const int backtraceSize = backtrace(backtraceArray, 20);
|
||||
|
||||
@@ -189,7 +189,8 @@ struct BattalionType {
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
|
||||
if (HasBridge(terrain.modifier())) {
|
||||
if (HasBridge(terrain.modifier()) && terrain.modifier().bridge().has_integrity() &&
|
||||
terrain.modifier().bridge().integrity().value() > 0.0) {
|
||||
return {ActionCost::standard,
|
||||
ActionPoints(std::ceil(
|
||||
100.0 * minimumCostToEnterBridge /
|
||||
@@ -208,7 +209,7 @@ struct BattalionType {
|
||||
[[nodiscard]] auto GetCostToEnterTerrain(
|
||||
const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType,
|
||||
const net::eagle0::shardok::storage::fb::TileModifier &tm) const -> ActionCost {
|
||||
if (tm.bridge().present()) {
|
||||
if (tm.bridge().present() && tm.bridge().integrity() > 0.0) {
|
||||
return {ActionCost::standard,
|
||||
ActionPoints(
|
||||
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity()))};
|
||||
|
||||
@@ -22,13 +22,13 @@ namespace shardok {
|
||||
using PercentileRollOdds = net::eagle0::shardok::storage::Odds;
|
||||
using OtherFactor = net::eagle0::shardok::storage::Odds_OtherFactor;
|
||||
|
||||
double BonusFromStat(double stat);
|
||||
[[nodiscard]] double BonusFromStat(double stat);
|
||||
|
||||
int16_t AverageBonusFromStats(const std::vector<double> &stats);
|
||||
[[nodiscard]] int16_t AverageBonusFromStats(const std::vector<double> &stats);
|
||||
|
||||
OtherFactor MakeOtherFactor(int value, const std::string &id);
|
||||
[[nodiscard]] OtherFactor MakeOtherFactor(int value, const std::string &id);
|
||||
|
||||
PercentileRollOdds MakeOdds(
|
||||
[[nodiscard]] PercentileRollOdds MakeOdds(
|
||||
int base = 0,
|
||||
int terr = 0,
|
||||
int weath = 0,
|
||||
@@ -36,7 +36,7 @@ PercentileRollOdds MakeOdds(
|
||||
int statF = 0,
|
||||
std::span<const OtherFactor> others = {});
|
||||
|
||||
PercentileRollOdds MakeOdds(
|
||||
[[nodiscard]] PercentileRollOdds MakeOdds(
|
||||
int base,
|
||||
int terr,
|
||||
int weath,
|
||||
@@ -44,11 +44,11 @@ PercentileRollOdds MakeOdds(
|
||||
const std::vector<double> &stats,
|
||||
std::span<const OtherFactor> others = {});
|
||||
|
||||
int16_t GetTotalOdds(const PercentileRollOdds &odds);
|
||||
[[nodiscard]] int16_t GetTotalOdds(const PercentileRollOdds &odds);
|
||||
|
||||
bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
|
||||
[[nodiscard]] bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
|
||||
|
||||
int16_t GetSuccessChance(const PercentileRollOdds &odds);
|
||||
[[nodiscard]] int16_t GetSuccessChance(const PercentileRollOdds &odds);
|
||||
} // namespace shardok
|
||||
|
||||
#endif // EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
|
||||
|
||||
@@ -718,8 +718,8 @@ auto SecuredByPlayerByUnitIdFromHistory(
|
||||
continue;
|
||||
}
|
||||
if (!resolvedUnit.has_secured_by_player()) { continue; }
|
||||
const auto *unit = reinterpret_cast<const Unit *>(resolvedUnit.unit_bytes().data());
|
||||
securedByPlayerByUnitId[unit->unit_id()] = resolvedUnit.secured_by_player().value();
|
||||
const auto unit = UnitFromBytes(resolvedUnit.unit_bytes());
|
||||
securedByPlayerByUnitId[unit.unit_id()] = resolvedUnit.secured_by_player().value();
|
||||
}
|
||||
}
|
||||
return securedByPlayerByUnitId;
|
||||
|
||||
+24
-30
@@ -14,6 +14,7 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/UnitHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
|
||||
|
||||
@@ -30,10 +31,6 @@ inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
|
||||
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
|
||||
}
|
||||
|
||||
inline auto UnitFromBytes(const std::string &unitBytes) -> const Unit & {
|
||||
return *reinterpret_cast<const Unit *>(unitBytes.data());
|
||||
}
|
||||
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition;
|
||||
using net::eagle0::shardok::storage::fb::VictoryType;
|
||||
|
||||
@@ -101,7 +98,7 @@ void ApplyResolvedUnit(
|
||||
GameStateT &inoutState,
|
||||
const ResolvedUnitProto &resolvedUnit,
|
||||
const double vipCapturedMoraleAdjustment) {
|
||||
const Unit &unit = UnitFromBytes(resolvedUnit.unit_bytes());
|
||||
const Unit unit = UnitFromBytes(resolvedUnit.unit_bytes());
|
||||
UnitId unitId = unit.unit_id();
|
||||
|
||||
if (unit.has_attached_hero() &&
|
||||
@@ -169,15 +166,15 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
|
||||
|
||||
// First pass: check what kind of modifications we need
|
||||
for (const auto &unitBytes : result.changed_units_fb()) {
|
||||
const auto *unit = &UnitFromBytes(unitBytes);
|
||||
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
|
||||
const auto unit = UnitFromBytes(unitBytes);
|
||||
maxChangedUnitId = std::max(maxChangedUnitId, unit.unit_id());
|
||||
|
||||
if (static_cast<unsigned int>(unit->unit_id()) >= mutatingState->units()->size()) {
|
||||
if (static_cast<unsigned int>(unit.unit_id()) >= mutatingState->units()->size()) {
|
||||
// Unit ID beyond vector size - must expand
|
||||
needsVectorExpansion = true;
|
||||
break; // No point checking further
|
||||
} else if (
|
||||
mutatingState->units()->Get(unit->unit_id())->status() ==
|
||||
mutatingState->units()->Get(unit.unit_id())->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
||||
// Unit wants to use a reserved slot
|
||||
needsReservedSlotConversion = true;
|
||||
@@ -199,8 +196,8 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
|
||||
// Convert reserved slots to real units in place
|
||||
// We only need to process the units that are being changed
|
||||
for (const auto &unitBytes : result.changed_units_fb()) {
|
||||
const auto *unit = &UnitFromBytes(unitBytes);
|
||||
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit->unit_id());
|
||||
const auto unit = UnitFromBytes(unitBytes);
|
||||
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit.unit_id());
|
||||
if (mutableUnit->status() ==
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
|
||||
// Convert this reserved slot to a real unit
|
||||
@@ -299,51 +296,48 @@ void MutatingApplyResult(
|
||||
const double vipCapturedMoraleAdjustment = settings.Backing().vip_captured_morale_adjustment();
|
||||
|
||||
for (const auto &changedUnitBytes : result.changed_units_fb()) {
|
||||
const Unit *changedUnit = &UnitFromBytes(changedUnitBytes);
|
||||
const Unit changedUnit = UnitFromBytes(changedUnitBytes);
|
||||
|
||||
internalAssert(
|
||||
changedUnit->unit_id() < static_cast<int32_t>(mutatingGameState->units()->size()),
|
||||
changedUnit.unit_id() < static_cast<int32_t>(mutatingGameState->units()->size()),
|
||||
"Got a changed unit outside acceptable range");
|
||||
const auto battalionSizeAfter = changedUnit->battalion().size();
|
||||
const auto battalionSizeAfter = changedUnit.battalion().size();
|
||||
|
||||
const int battalionSizeBefore =
|
||||
(changedUnit->unit_id() < static_cast<int32_t>(initialUnitCount))
|
||||
? mutatingGameState->units()
|
||||
->Get(changedUnit->unit_id())
|
||||
->battalion()
|
||||
.size()
|
||||
: changedUnit->battalion().size();
|
||||
(changedUnit.unit_id() < static_cast<int32_t>(initialUnitCount))
|
||||
? mutatingGameState->units()->Get(changedUnit.unit_id())->battalion().size()
|
||||
: changedUnit.battalion().size();
|
||||
|
||||
auto status = changedUnit->status();
|
||||
if (IsDestroyed(*changedUnit)) {
|
||||
auto status = changedUnit.status();
|
||||
if (IsDestroyed(changedUnit)) {
|
||||
status =
|
||||
changedUnit->has_attached_hero()
|
||||
changedUnit.has_attached_hero()
|
||||
? net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT
|
||||
: net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT;
|
||||
|
||||
ApplyResolvedUnit(
|
||||
mutatingGameState.Get(),
|
||||
*changedUnit,
|
||||
changedUnit,
|
||||
status,
|
||||
vipCapturedMoraleAdjustment,
|
||||
settings);
|
||||
}
|
||||
|
||||
// Capture old position before applying changes
|
||||
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit->unit_id());
|
||||
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit.unit_id());
|
||||
const auto oldLocation = mutableUnit->location();
|
||||
|
||||
fb::ApplyUnit(mutableUnit, changedUnit, status);
|
||||
fb::ApplyUnit(mutableUnit, &changedUnit, status);
|
||||
|
||||
// Update occupied tiles bitfield if position changed
|
||||
const auto &newLocation = changedUnit->location();
|
||||
const auto &newLocation = changedUnit.location();
|
||||
if (oldLocation.row() != newLocation.row() ||
|
||||
oldLocation.column() != newLocation.column()) {
|
||||
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
|
||||
}
|
||||
|
||||
if (battalionSizeBefore != battalionSizeAfter) {
|
||||
if (changedUnit->battalion().type() ==
|
||||
if (changedUnit.battalion().type() ==
|
||||
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
|
||||
if (battalionSizeAfter > battalionSizeBefore) {
|
||||
// Undead consumed some bodies
|
||||
@@ -355,7 +349,7 @@ void MutatingApplyResult(
|
||||
if (battalionSizeAfter > battalionSizeBefore) {
|
||||
std::ostringstream stringStream;
|
||||
stringStream << "Battalion got larger! Was " << battalionSizeBefore << ", now "
|
||||
<< changedUnit << " result was " << result.DebugString();
|
||||
<< &changedUnit << " result was " << result.DebugString();
|
||||
throw ShardokInternalErrorException(stringStream.str());
|
||||
}
|
||||
|
||||
@@ -365,7 +359,7 @@ void MutatingApplyResult(
|
||||
if (newCount != 0) {
|
||||
newArmament =
|
||||
(mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
|
||||
casualties * changedUnit->battalion().armament()) /
|
||||
casualties * changedUnit.battalion().armament()) /
|
||||
newCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_hasher",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/unit",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
|
||||
@@ -60,7 +60,8 @@ auto StartPlayerTurnAction::InternalExecute(
|
||||
->Get(startingState->month() - 1)
|
||||
->is_winter() &&
|
||||
!inCastle) {
|
||||
effectiveBase += float(settingsGetter.Backing().winter_morale_adjustment());
|
||||
effectiveBase +=
|
||||
static_cast<float>(settingsGetter.Backing().winter_morale_adjustment());
|
||||
}
|
||||
float moraleDiff = effectiveBase - updatedUnit.battalion().morale();
|
||||
MutatingMaybeAdjustMorale(
|
||||
|
||||
@@ -34,7 +34,8 @@ auto UndeadChangeAction::InternalExecute(
|
||||
int growth = newCount - decayedSize;
|
||||
|
||||
float newArmament =
|
||||
float(unit->battalion().armament() * decayedSize + armament * growth) / newCount;
|
||||
static_cast<float>(unit->battalion().armament() * decayedSize + armament * growth) /
|
||||
newCount;
|
||||
|
||||
auto newUnit = *unit;
|
||||
newUnit.mutable_battalion().mutate_size(newCount);
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
namespace shardok {
|
||||
|
||||
class EvacuatePrisonersCommandFactory : public CommandFactory {
|
||||
const SettingsGetter& settings;
|
||||
const SettingsGetter settings;
|
||||
|
||||
public:
|
||||
explicit EvacuatePrisonersCommandFactory(const SettingsGetter& getter);
|
||||
|
||||
@@ -61,9 +61,11 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
const auto *occupant = Occupant(allUnits, twoAwayCoords);
|
||||
if (occupant && !occupant->hidden()) {
|
||||
if (std::ranges::contains(allyPids, occupant->player_id())) {
|
||||
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
|
||||
adjacentFriendliesMod +=
|
||||
static_cast<int32_t>(perAdjacentFriendly * adjustmentForTwoAway);
|
||||
} else {
|
||||
adjacentEnemiesMod += int32_t(perAdjacentEnemy * adjustmentForTwoAway);
|
||||
adjacentEnemiesMod +=
|
||||
static_cast<int32_t>(perAdjacentEnemy * adjustmentForTwoAway);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ auto HolyWaveDamageAction::InternalExecute(
|
||||
resultProto.set_type(net::eagle0::shardok::common::HOLY_WAVE_DAMAGE);
|
||||
|
||||
const Unit *damagedUnit = currentState->units()->Get(damagedUnitId);
|
||||
auto newSize = int32_t(damagedUnit->battalion().size() * (1.0 - damagePercentage));
|
||||
auto newSize = static_cast<int32_t>(damagedUnit->battalion().size() * (1.0 - damagePercentage));
|
||||
|
||||
auto damagedUnitAfter = *damagedUnit;
|
||||
damagedUnitAfter.mutable_battalion().mutate_size(newSize);
|
||||
|
||||
@@ -171,7 +171,8 @@ auto SetupInitialGameState(
|
||||
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
|
||||
if (modifiedUnit.can_archery()) {
|
||||
modifiedUnit.mutate_volleys_remaining(
|
||||
int32_t(modifiedUnit.battalion().armament() *
|
||||
static_cast<int32_t>(
|
||||
modifiedUnit.battalion().armament() *
|
||||
settings.Backing().armament_to_volleys()) +
|
||||
settings.GetBattalionType(modifiedUnit.battalion().type())->bonusVolleys);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
@@ -28,6 +28,14 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "coords_set_benchmark",
|
||||
srcs = ["CoordsSetBenchmark.cpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":coords_set"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "hex_map_direction",
|
||||
hdrs = ["HexMapDirection.hpp"],
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace std {
|
||||
template<>
|
||||
struct hash<net::eagle0::shardok::common::Coords> {
|
||||
[[nodiscard]] auto operator()(const net::eagle0::shardok::common::Coords &c) const -> size_t {
|
||||
return size_t(c.row() << 16 | c.column());
|
||||
return static_cast<size_t>(c.row() << 16 | c.column());
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#define EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP
|
||||
|
||||
#include <bit>
|
||||
#include <functional>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -17,7 +20,11 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
#ifndef NDEBUG
|
||||
constexpr bool kCoordsSetIndexChecks = true;
|
||||
#else
|
||||
constexpr bool kCoordsSetIndexChecks = false;
|
||||
#endif
|
||||
|
||||
constexpr size_t STANDARD_SIZE = size_t{12} * 14;
|
||||
|
||||
@@ -28,13 +35,28 @@ class CoordsSet {
|
||||
private:
|
||||
BackingBitset store;
|
||||
|
||||
int mapWidth;
|
||||
int mapHeight;
|
||||
uint8_t columnCount;
|
||||
uint8_t rowCount;
|
||||
|
||||
uint32_t indexCount;
|
||||
[[maybe_unused]] uint16_t indexCount;
|
||||
|
||||
void AssertCoordsInBounds([[maybe_unused]] const int row, [[maybe_unused]] const int column)
|
||||
const {
|
||||
if constexpr (kCoordsSetIndexChecks) {
|
||||
assert(row >= 0);
|
||||
assert(column >= 0);
|
||||
assert(row < rowCount);
|
||||
assert(column < columnCount);
|
||||
}
|
||||
}
|
||||
|
||||
void AssertIndexInBounds([[maybe_unused]] const uint32_t idx) const {
|
||||
if constexpr (kCoordsSetIndexChecks) { assert(idx < indexCount); }
|
||||
}
|
||||
|
||||
[[nodiscard]] auto Index(const int row, const int column) const -> int {
|
||||
return (row * mapWidth) + column;
|
||||
AssertCoordsInBounds(row, column);
|
||||
return (row * columnCount) + column;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto Index(const Coords& c) const -> int { return Index(c.row(), c.column()); }
|
||||
@@ -49,18 +71,25 @@ private:
|
||||
};
|
||||
|
||||
void Remove(uint32_t idx) {
|
||||
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
|
||||
AssertIndexInBounds(idx);
|
||||
store.Unset(idx);
|
||||
}
|
||||
[[nodiscard]] auto Contains(uint32_t idx) const -> bool {
|
||||
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
|
||||
AssertIndexInBounds(idx);
|
||||
return store.IsSet(idx);
|
||||
}
|
||||
|
||||
public:
|
||||
CoordsSet(int w, int h) : mapWidth(w), mapHeight(h), indexCount(w * h) {
|
||||
if (kCoordsSetIndexChecks) {
|
||||
assert(static_cast<size_t>(w) * static_cast<size_t>(h) < STANDARD_SIZE);
|
||||
CoordsSet(const int w, const int h)
|
||||
: columnCount(static_cast<uint8_t>(w)),
|
||||
rowCount(static_cast<uint8_t>(h)),
|
||||
indexCount(static_cast<uint16_t>(w * h)) {
|
||||
if constexpr (kCoordsSetIndexChecks) {
|
||||
assert(w >= 0);
|
||||
assert(h >= 0);
|
||||
assert(w <= std::numeric_limits<uint8_t>::max());
|
||||
assert(h <= std::numeric_limits<uint8_t>::max());
|
||||
assert(static_cast<size_t>(w) * static_cast<size_t>(h) <= STANDARD_SIZE);
|
||||
}
|
||||
clear();
|
||||
}
|
||||
@@ -74,15 +103,20 @@ public:
|
||||
auto operator=(const CoordsSet& rhs) -> CoordsSet& = default;
|
||||
auto operator=(CoordsSet&& rhs) -> CoordsSet& = default;
|
||||
|
||||
auto operator==(const CoordsSet& rhs) const -> bool {
|
||||
if (mapWidth != rhs.mapWidth) return false;
|
||||
if (mapHeight != rhs.mapHeight) return false;
|
||||
[[nodiscard]] auto operator==(const CoordsSet& rhs) const -> bool {
|
||||
if (columnCount != rhs.columnCount) return false;
|
||||
if (rowCount != rhs.rowCount) return false;
|
||||
return store == rhs.store;
|
||||
}
|
||||
|
||||
void Add(const Coords& c) { AddIndex(Index(c)); }
|
||||
void Add(const int row, const int column) { AddIndex(Index(row, column)); }
|
||||
void AddIndex(const uint32_t idx) { store.Set(idx); }
|
||||
void Add(const Coords& c) { AddMapIndexUnchecked(Index(c)); }
|
||||
void Add(const int row, const int column) { AddMapIndexUnchecked(Index(row, column)); }
|
||||
|
||||
// For callers that already have row * column_count + column.
|
||||
void AddMapIndexUnchecked(const uint32_t idx) {
|
||||
AssertIndexInBounds(idx);
|
||||
store.Set(idx);
|
||||
}
|
||||
|
||||
// Always succeeds
|
||||
void Remove(const Coords& c) { Remove(Index(c)); }
|
||||
@@ -101,13 +135,15 @@ public:
|
||||
|
||||
[[nodiscard]] auto empty() const -> bool { return store.empty(); }
|
||||
|
||||
[[nodiscard]] auto ColumnCount() const -> int { return columnCount; }
|
||||
|
||||
// Should only be used for debugging.
|
||||
[[nodiscard]] auto as_vector() const -> std::vector<Coords> {
|
||||
return std::vector<Coords>{this->begin(), this->end()};
|
||||
}
|
||||
|
||||
auto operator+=(const CoordsSet& rhs) -> CoordsSet& {
|
||||
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
|
||||
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
|
||||
throw MismatchException("mismatched sizes");
|
||||
}
|
||||
|
||||
@@ -115,14 +151,14 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator+(const CoordsSet& rhs) const -> CoordsSet {
|
||||
[[nodiscard]] auto operator+(const CoordsSet& rhs) const -> CoordsSet {
|
||||
CoordsSet cs = *this;
|
||||
cs += rhs;
|
||||
return cs;
|
||||
}
|
||||
|
||||
auto operator-=(const CoordsSet& rhs) -> CoordsSet& {
|
||||
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
|
||||
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
|
||||
throw MismatchException("mismatched sizes");
|
||||
}
|
||||
|
||||
@@ -130,15 +166,25 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator-(const CoordsSet& rhs) const -> CoordsSet {
|
||||
[[nodiscard]] auto operator-(const CoordsSet& rhs) const -> CoordsSet {
|
||||
CoordsSet cs = *this;
|
||||
cs -= rhs;
|
||||
return cs;
|
||||
}
|
||||
|
||||
void Filter(const std::function<bool(int, int)>& f) {
|
||||
for (uint32_t i = 0; i < indexCount; ++i) {
|
||||
if (Contains(i) && !f(i / mapWidth, i % mapWidth)) { Remove(i); }
|
||||
template<class Func>
|
||||
void Filter(Func&& f) {
|
||||
if (size() * 4 > indexCount) {
|
||||
for (uint32_t i = 0; i < indexCount; ++i) {
|
||||
if (Contains(i) && !f(i / columnCount, i % columnCount)) { Remove(i); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto iter = store.begin(); iter != store.end();) {
|
||||
const uint32_t i = static_cast<uint32_t>(*iter);
|
||||
++iter;
|
||||
if (!f(i / columnCount, i % columnCount)) { Remove(i); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +192,9 @@ public:
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = Coords;
|
||||
using difference_type = int;
|
||||
using pointer = Coords*;
|
||||
using reference = Coords&;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = void;
|
||||
using reference = Coords;
|
||||
|
||||
private:
|
||||
BackingBitset::Iter iterator;
|
||||
@@ -158,15 +204,28 @@ public:
|
||||
public:
|
||||
Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(cs) {}
|
||||
|
||||
auto operator==(const Iter& rhs) const -> bool { return iterator == rhs.iterator; }
|
||||
auto operator!=(const Iter& rhs) const -> bool { return iterator != rhs.iterator; }
|
||||
void operator++() { ++iterator; }
|
||||
[[nodiscard]] auto operator==(const Iter& rhs) const -> bool {
|
||||
return iterator == rhs.iterator;
|
||||
}
|
||||
[[nodiscard]] auto operator!=(const Iter& rhs) const -> bool {
|
||||
return iterator != rhs.iterator;
|
||||
}
|
||||
auto operator++() -> Iter& {
|
||||
++iterator;
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto operator*() const -> Coords {
|
||||
auto operator++(int) -> Iter {
|
||||
Iter previous = *this;
|
||||
++(*this);
|
||||
return previous;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto operator*() const -> Coords {
|
||||
const size_t index = *iterator;
|
||||
|
||||
const int row = index / cs.mapWidth;
|
||||
const int column = index % cs.mapWidth;
|
||||
const int row = static_cast<int>(index / static_cast<size_t>(cs.columnCount));
|
||||
const int column = static_cast<int>(index % static_cast<size_t>(cs.columnCount));
|
||||
return Coords(row, column);
|
||||
}
|
||||
};
|
||||
@@ -185,25 +244,21 @@ public:
|
||||
[[nodiscard]] auto end() const -> BackingBitset::Iter { return cs.store.end(); }
|
||||
};
|
||||
[[nodiscard]] auto indexIterator() const -> IndexIterator { return IndexIterator(*this); }
|
||||
|
||||
[[nodiscard]] auto ToString() const -> std::string {
|
||||
return std::string(reinterpret_cast<char const*>(&store), STANDARD_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
namespace common {
|
||||
static inline auto Contains(const shardok::CoordsSet& container, const shardok::Coords& elt)
|
||||
-> bool {
|
||||
[[nodiscard]] static inline auto Contains(
|
||||
const shardok::CoordsSet& container,
|
||||
const shardok::Coords& elt) -> bool {
|
||||
return container.Contains(elt);
|
||||
}
|
||||
|
||||
template<class Func>
|
||||
void FilterInPlace(shardok::CoordsSet& container, Func fn) {
|
||||
for (const shardok::Coords& c : container) {
|
||||
if (!fn(c)) container.Remove(c);
|
||||
}
|
||||
container.Filter(
|
||||
[&fn](const int row, const int column) { return fn(shardok::Coords(row, column)); });
|
||||
}
|
||||
} // namespace common
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
constexpr int kColumnCount = 12;
|
||||
constexpr int kRowCount = 14;
|
||||
constexpr int kIndexCount = kColumnCount * kRowCount;
|
||||
|
||||
volatile uint64_t benchmarkSink = 0;
|
||||
|
||||
template<class T>
|
||||
void DoNotOptimize(const T& value) {
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
asm volatile("" : : "g"(&value) : "memory");
|
||||
#else
|
||||
benchmarkSink ^= reinterpret_cast<uintptr_t>(&value);
|
||||
#endif
|
||||
}
|
||||
|
||||
struct BenchmarkResult {
|
||||
std::string_view name;
|
||||
int iterations;
|
||||
std::chrono::nanoseconds elapsed;
|
||||
uint64_t checksum;
|
||||
};
|
||||
|
||||
void Consume(uint64_t checksum) { benchmarkSink = checksum; }
|
||||
|
||||
[[nodiscard]] auto MakePatternedCoordsSet(const int step, const int offset = 0) -> CoordsSet {
|
||||
CoordsSet coords(kColumnCount, kRowCount);
|
||||
for (int index = offset; index < kIndexCount; index += step) {
|
||||
coords.AddMapIndexUnchecked(static_cast<uint32_t>(index));
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
|
||||
template<class Func>
|
||||
[[nodiscard]] auto RunBenchmark(std::string_view name, const int iterations, Func func)
|
||||
-> BenchmarkResult {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const uint64_t checksum = func(iterations);
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
Consume(checksum);
|
||||
return BenchmarkResult{
|
||||
name,
|
||||
iterations,
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start),
|
||||
checksum};
|
||||
}
|
||||
|
||||
void PrintResult(const BenchmarkResult& result) {
|
||||
const double nsPerIteration =
|
||||
static_cast<double>(result.elapsed.count()) / static_cast<double>(result.iterations);
|
||||
std::cout << std::left << std::setw(28) << result.name << " iterations=" << std::right
|
||||
<< std::setw(10) << result.iterations << " ns/op=" << std::fixed
|
||||
<< std::setprecision(2) << std::setw(10) << nsPerIteration
|
||||
<< " checksum=" << result.checksum << '\n';
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkCopies(const int iterations) -> uint64_t {
|
||||
const CoordsSet source = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet copy = source;
|
||||
DoNotOptimize(copy);
|
||||
checksum += copy.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkUnions(const int iterations) -> uint64_t {
|
||||
const CoordsSet left = MakePatternedCoordsSet(3);
|
||||
const CoordsSet right = MakePatternedCoordsSet(5, 1);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet combined = left;
|
||||
combined += right;
|
||||
DoNotOptimize(combined);
|
||||
checksum += combined.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkSubtractions(const int iterations) -> uint64_t {
|
||||
const CoordsSet full = MakePatternedCoordsSet(1);
|
||||
const CoordsSet removed = MakePatternedCoordsSet(4);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet remaining = full;
|
||||
remaining -= removed;
|
||||
DoNotOptimize(remaining);
|
||||
checksum += remaining.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkContains(const int iterations) -> uint64_t {
|
||||
const CoordsSet coords = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
const int index = i % kIndexCount;
|
||||
checksum += coords.Contains(index / kColumnCount, index % kColumnCount) ? 1 : 0;
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkCoordinateIteration(const int iterations) -> uint64_t {
|
||||
const CoordsSet coords = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
for (const auto& coord : coords) {
|
||||
checksum += static_cast<uint64_t>(coord.row() + coord.column());
|
||||
}
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkIndexIteration(const int iterations) -> uint64_t {
|
||||
const CoordsSet coords = MakePatternedCoordsSet(3);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
for (const auto index : coords.indexIterator()) { checksum += index; }
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkDenseFilter(const int iterations) -> uint64_t {
|
||||
const CoordsSet source = MakePatternedCoordsSet(1);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet filtered = source;
|
||||
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
|
||||
DoNotOptimize(filtered);
|
||||
checksum += filtered.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto BenchmarkSparseFilter(const int iterations) -> uint64_t {
|
||||
const CoordsSet source = MakePatternedCoordsSet(7);
|
||||
uint64_t checksum = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
CoordsSet filtered = source;
|
||||
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
|
||||
DoNotOptimize(filtered);
|
||||
checksum += filtered.size();
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace shardok
|
||||
|
||||
auto main() -> int {
|
||||
using shardok::BenchmarkContains;
|
||||
using shardok::BenchmarkCoordinateIteration;
|
||||
using shardok::BenchmarkCopies;
|
||||
using shardok::BenchmarkDenseFilter;
|
||||
using shardok::BenchmarkIndexIteration;
|
||||
using shardok::BenchmarkSparseFilter;
|
||||
using shardok::BenchmarkSubtractions;
|
||||
using shardok::BenchmarkUnions;
|
||||
using shardok::CoordsSet;
|
||||
using shardok::PrintResult;
|
||||
using shardok::RunBenchmark;
|
||||
|
||||
std::cout << "CoordsSet benchmark\n";
|
||||
std::cout << "sizeof(CoordsSet)=" << sizeof(CoordsSet)
|
||||
<< " alignof(CoordsSet)=" << alignof(CoordsSet) << '\n';
|
||||
|
||||
PrintResult(RunBenchmark("copy", 5'000'000, BenchmarkCopies));
|
||||
PrintResult(RunBenchmark("union", 3'000'000, BenchmarkUnions));
|
||||
PrintResult(RunBenchmark("subtraction", 3'000'000, BenchmarkSubtractions));
|
||||
PrintResult(RunBenchmark("contains", 20'000'000, BenchmarkContains));
|
||||
PrintResult(RunBenchmark("coord iteration", 1'000'000, BenchmarkCoordinateIteration));
|
||||
PrintResult(RunBenchmark("index iteration", 1'000'000, BenchmarkIndexIteration));
|
||||
PrintResult(RunBenchmark("dense filter", 500'000, BenchmarkDenseFilter));
|
||||
PrintResult(RunBenchmark("sparse filter", 500'000, BenchmarkSparseFilter));
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ using TileModifierProto = net::eagle0::shardok::common::TileModifier;
|
||||
|
||||
SingleModifier MakeSingleModifier(const std::optional<double>& integ = std::nullopt);
|
||||
|
||||
bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
|
||||
[[nodiscard]] bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
|
||||
|
||||
TileModifierProto MakeTileModifier(bool onFire = false);
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
namespace shardok {
|
||||
using TileModifierWithCoords = net::eagle0::shardok::common::TileModifierWithCoords;
|
||||
|
||||
auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
|
||||
[[nodiscard]] auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
|
||||
-> TileModifierWithCoords;
|
||||
|
||||
auto MakeTmc(
|
||||
[[nodiscard]] auto MakeTmc(
|
||||
const Coords &coordinates,
|
||||
const net::eagle0::shardok::storage::fb::TileModifier &modifier) -> TileModifierWithCoords;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace shardok {
|
||||
|
||||
class SettingsLoader {
|
||||
public:
|
||||
static auto LoadSettings() -> std::shared_ptr<GameSettings>;
|
||||
[[nodiscard]] static auto LoadSettings() -> std::shared_ptr<GameSettings>;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
#ifndef EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
|
||||
#define EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
@@ -16,13 +19,30 @@ using ActionResult = net::eagle0::shardok::storage::ActionResult;
|
||||
using ResolvedUnit = net::eagle0::shardok::storage::ResolvedUnit;
|
||||
using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
static_assert(std::is_trivially_copyable_v<Unit>);
|
||||
|
||||
[[nodiscard]] static inline auto UnitToBytes(const Unit& unit) -> std::string {
|
||||
std::string bytes(sizeof(Unit), '\0');
|
||||
std::memcpy(bytes.data(), &unit, sizeof(Unit));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
[[nodiscard]] static inline auto UnitFromBytes(const std::string& unitBytes) -> Unit {
|
||||
if (unitBytes.size() != sizeof(Unit)) {
|
||||
throw std::invalid_argument("Unexpected unit byte length");
|
||||
}
|
||||
|
||||
Unit unit{};
|
||||
std::memcpy(&unit, unitBytes.data(), sizeof(Unit));
|
||||
return unit;
|
||||
}
|
||||
|
||||
static inline void AddChangedUnit(ActionResult& result, const Unit& unit) {
|
||||
result.add_changed_units_fb(&unit, sizeof(Unit));
|
||||
}
|
||||
|
||||
static inline void AddUnitToResolved(ResolvedUnit& resolvedUnit, const Unit& unit) {
|
||||
*resolvedUnit.mutable_unit_bytes() =
|
||||
std::string(reinterpret_cast<const char*>(&unit), sizeof(Unit));
|
||||
*resolvedUnit.mutable_unit_bytes() = UnitToBytes(unit);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -6,6 +6,7 @@ cc_library(
|
||||
hdrs = ["ActionResultFlatbufferHelpers.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = [
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
|
||||
],
|
||||
|
||||
@@ -306,10 +306,9 @@ auto MakeActionResultFb(const ActionType type, const MeleeResultFb &results, con
|
||||
ar.mutable_actor()->set_value(results.attackerAfter.unit_id());
|
||||
ar.mutable_target_unit()->set_value(results.defenderAfter.unit_id());
|
||||
|
||||
AddChangedUnit(ar, results.attackerAfter);
|
||||
auto *attCU = reinterpret_cast<Unit *>(
|
||||
ar.mutable_changed_units_fb(ar.changed_units_fb_size() - 1)->data());
|
||||
if (resetZoc) attCU->mutate_has_moved_in_zoc(false);
|
||||
Unit attackerAfter = results.attackerAfter;
|
||||
if (resetZoc) attackerAfter.mutate_has_moved_in_zoc(false);
|
||||
AddChangedUnit(ar, attackerAfter);
|
||||
|
||||
AddChangedUnit(ar, results.defenderAfter);
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ const std::vector<CoordsSet> HexMapUtils::standardCoords =
|
||||
|
||||
auto HexMapUtils::ComputeAllStandardAdjacentCoords() -> std::vector<CoordsSet> {
|
||||
std::vector<CoordsSet> arr;
|
||||
arr.reserve(size_t(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
|
||||
arr.reserve(static_cast<size_t>(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
|
||||
|
||||
for (int index = 0; index < STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS; ++index) {
|
||||
arr.push_back(ComputeAdjacentCoords(
|
||||
@@ -127,25 +127,25 @@ auto HexMapUtils::ComputeAdjacentCoords(
|
||||
|
||||
// Row above
|
||||
if (origin.row() > 0) {
|
||||
cs.AddIndex(originIndex - columnCount);
|
||||
cs.AddMapIndexUnchecked(originIndex - columnCount);
|
||||
|
||||
if (origin.row() % 2 == 0) {
|
||||
if (origin.column() > 0) { cs.AddIndex(originIndex - columnCount - 1); }
|
||||
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - columnCount - 1); }
|
||||
} else if (origin.column() < columnCount - 1) {
|
||||
cs.AddIndex(originIndex - columnCount + 1);
|
||||
cs.AddMapIndexUnchecked(originIndex - columnCount + 1);
|
||||
}
|
||||
}
|
||||
// This row
|
||||
if (origin.column() > 0) { cs.AddIndex(originIndex - 1); }
|
||||
if (origin.column() < columnCount - 1) { cs.AddIndex(originIndex + 1); }
|
||||
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - 1); }
|
||||
if (origin.column() < columnCount - 1) { cs.AddMapIndexUnchecked(originIndex + 1); }
|
||||
// Row below
|
||||
if (origin.row() < rowCount - 1) {
|
||||
cs.AddIndex(originIndex + columnCount);
|
||||
cs.AddMapIndexUnchecked(originIndex + columnCount);
|
||||
|
||||
if (origin.row() % 2 == 0) {
|
||||
if (origin.column() > 0) { cs.AddIndex(originIndex + columnCount - 1); }
|
||||
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex + columnCount - 1); }
|
||||
} else if (origin.column() < columnCount - 1) {
|
||||
cs.AddIndex(originIndex + columnCount + 1);
|
||||
cs.AddMapIndexUnchecked(originIndex + columnCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ struct alignas(16) AdjacentTile {
|
||||
|
||||
AdjacentTile(const Coords co, const HexMapDirection dir) : directionTo(dir), coords(co) {}
|
||||
|
||||
auto operator==(const AdjacentTile &rhs) const -> bool {
|
||||
[[nodiscard]] auto operator==(const AdjacentTile &rhs) const -> bool {
|
||||
return coords == rhs.coords && directionTo == rhs.directionTo;
|
||||
}
|
||||
};
|
||||
@@ -73,21 +73,21 @@ public:
|
||||
|
||||
[[nodiscard]] auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
auto HasForestAccess(
|
||||
[[nodiscard]] auto HasForestAccess(
|
||||
const Coords &coords,
|
||||
const Units *units,
|
||||
const HexMap *hexMap,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
PlayerId player) -> bool;
|
||||
|
||||
auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
|
||||
auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
|
||||
[[nodiscard]] auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
|
||||
[[nodiscard]] auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
|
||||
// Overload for const HexMap - uses const_cast internally. Safe when the underlying buffer is
|
||||
// mutable.
|
||||
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
|
||||
[[nodiscard]] auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
|
||||
|
||||
auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
|
||||
auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
|
||||
[[nodiscard]] auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
|
||||
[[nodiscard]] auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
|
||||
|
||||
[[nodiscard]] auto GetTilesAcrossWater(const HexMap *map, const Coords &coords) -> CoordsSet;
|
||||
|
||||
@@ -100,7 +100,8 @@ auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool
|
||||
[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &coords)
|
||||
-> CoordsSet;
|
||||
|
||||
static inline auto Occupant(const Units *units, const Coords &coords) -> const Unit * {
|
||||
[[nodiscard]] static inline auto Occupant(const Units *units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
for (const auto &unit : *units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
@@ -111,7 +112,8 @@ static inline auto Occupant(const Units *units, const Coords &coords) -> const U
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline auto Occupant(const Units &units, const Coords &coords) -> const Unit * {
|
||||
[[nodiscard]] static inline auto Occupant(const Units &units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
@@ -122,7 +124,7 @@ static inline auto Occupant(const Units &units, const Coords &coords) -> const U
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline auto Occupants(
|
||||
[[nodiscard]] static inline auto Occupants(
|
||||
const std::vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> std::vector<const Unit *> {
|
||||
@@ -138,7 +140,7 @@ static inline auto Occupants(
|
||||
return positions;
|
||||
}
|
||||
|
||||
static inline auto Occupants(
|
||||
[[nodiscard]] static inline auto Occupants(
|
||||
const flatbuffers::Vector<const Unit *> &units,
|
||||
const int rowCount,
|
||||
const int columnCount) -> std::vector<const Unit *> {
|
||||
@@ -154,8 +156,9 @@ static inline auto Occupants(
|
||||
return positions;
|
||||
}
|
||||
|
||||
static inline auto Occupant(const std::vector<const Unit *> &units, const Coords &coords)
|
||||
-> const Unit * {
|
||||
[[nodiscard]] static inline auto Occupant(
|
||||
const std::vector<const Unit *> &units,
|
||||
const Coords &coords) -> const Unit * {
|
||||
for (const auto &unit : units) {
|
||||
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
|
||||
unit->location() == coords) {
|
||||
@@ -167,7 +170,7 @@ static inline auto Occupant(const std::vector<const Unit *> &units, const Coords
|
||||
}
|
||||
|
||||
template<class UnitContainer>
|
||||
auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
-> std::optional<typename UnitContainer::value_type> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
@@ -177,7 +180,8 @@ auto Occupant(const UnitContainer &units, const Coords &coords)
|
||||
}
|
||||
|
||||
template<class U>
|
||||
auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::optional<U> {
|
||||
[[nodiscard]] auto Occupant(const std::map<UnitId, U> &units, const Coords &coords)
|
||||
-> std::optional<U> {
|
||||
for (const auto &[unitId, unit] : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
@@ -186,7 +190,7 @@ auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::op
|
||||
}
|
||||
|
||||
template<class U>
|
||||
auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
|
||||
[[nodiscard]] auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
|
||||
-> std::optional<U> {
|
||||
for (const auto &[unitId, unit] : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
@@ -196,7 +200,7 @@ auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coo
|
||||
}
|
||||
|
||||
template<class U, class UnitContainer>
|
||||
auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
|
||||
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
|
||||
for (const auto &unit : units) {
|
||||
if (unit.location() == coords) { return unit; }
|
||||
}
|
||||
@@ -204,7 +208,7 @@ auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static inline auto FriendlyOccupant(
|
||||
[[nodiscard]] static inline auto FriendlyOccupant(
|
||||
const PlayerId pid,
|
||||
const Units *units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
@@ -221,7 +225,7 @@ static inline auto FriendlyOccupant(
|
||||
}
|
||||
|
||||
template<class U, class UnitContainer>
|
||||
auto FriendlyOccupant(
|
||||
[[nodiscard]] auto FriendlyOccupant(
|
||||
PlayerId pid,
|
||||
const UnitContainer &units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
@@ -237,14 +241,14 @@ auto FriendlyOccupant(
|
||||
return {};
|
||||
}
|
||||
|
||||
auto KnownEnemyOccupant(
|
||||
[[nodiscard]] auto KnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> const Unit *;
|
||||
|
||||
template<class UnitContainer>
|
||||
auto KnownEnemyOccupant(
|
||||
[[nodiscard]] auto KnownEnemyOccupant(
|
||||
PlayerId playerId,
|
||||
const UnitContainer &units,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
@@ -277,31 +281,35 @@ auto KnownEnemyOccupant(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto KnownAdjacentEnemies(
|
||||
[[nodiscard]] auto KnownAdjacentEnemies(
|
||||
PlayerId playerId,
|
||||
const Units *units,
|
||||
const HexMap *map,
|
||||
const std::vector<PlayerId> &allyPids,
|
||||
const Coords &coords) -> std::vector<const Unit *>;
|
||||
|
||||
auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
|
||||
[[nodiscard]] auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
|
||||
-> HexMapDirectionsTo;
|
||||
|
||||
static inline auto DirectionsTo(const HexMap *map, const Coords &origin, const Coords &destination)
|
||||
-> HexMapDirectionsTo {
|
||||
[[nodiscard]] static inline auto DirectionsTo(
|
||||
const HexMap *map,
|
||||
const Coords &origin,
|
||||
const Coords &destination) -> HexMapDirectionsTo {
|
||||
return DirectionsTo(map->column_count(), origin, destination);
|
||||
}
|
||||
|
||||
auto MaybeTileInDirection(const HexMap *map, const Coords &inCoords, HexMapDirection orientation)
|
||||
-> std::optional<Coords>;
|
||||
[[nodiscard]] auto MaybeTileInDirection(
|
||||
const HexMap *map,
|
||||
const Coords &inCoords,
|
||||
HexMapDirection orientation) -> std::optional<Coords>;
|
||||
|
||||
auto MaybeOccupantInDirection(
|
||||
[[nodiscard]] auto MaybeOccupantInDirection(
|
||||
const HexMap *map,
|
||||
const Units *units,
|
||||
const Coords &coords,
|
||||
HexMapDirection direction) -> const Unit *;
|
||||
|
||||
auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
[[nodiscard]] auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
void MutatingSetTileModifier(
|
||||
HexMapT *hexMap,
|
||||
@@ -318,9 +326,10 @@ void MutatingSetTileModifier(
|
||||
const CoordsProto &coords,
|
||||
const TileModifierProto &TileModifierProto);
|
||||
|
||||
auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
|
||||
[[nodiscard]] auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
|
||||
|
||||
auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units) -> bool;
|
||||
[[nodiscard]] auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units)
|
||||
-> bool;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ namespace shardok {
|
||||
using PlayerInfoFb = net::eagle0::shardok::storage::fb::PlayerInfo;
|
||||
using GameState = net::eagle0::shardok::storage::fb::GameState;
|
||||
|
||||
auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
|
||||
auto NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true)
|
||||
-> PlayerId;
|
||||
auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
|
||||
[[nodiscard]] auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
|
||||
[[nodiscard]] auto
|
||||
NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true) -> PlayerId;
|
||||
[[nodiscard]] auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <ranges>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateViewDiffer.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/action_type.pb.h"
|
||||
@@ -87,7 +88,7 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
|
||||
if (result.has_actor()) {
|
||||
const auto actorId = result.actor().value();
|
||||
for (const auto &unitData : result.changed_units_fb()) {
|
||||
const auto &unit = *reinterpret_cast<const Unit *>(unitData.data());
|
||||
const auto unit = UnitFromBytes(unitData);
|
||||
if (unit.unit_id() == actorId) {
|
||||
opponentKnowledgeOfActor = OpponentKnowledge(unit, askingPlayer);
|
||||
hiddenActor = unit.hidden();
|
||||
|
||||
@@ -13,6 +13,7 @@ cc_library(
|
||||
deps = [
|
||||
":unit_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_view_differ",
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_cc_proto",
|
||||
|
||||
@@ -70,7 +70,8 @@ auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &aft
|
||||
|
||||
for (int tileIndex = 0; tileIndex < afterCount; ++tileIndex) {
|
||||
Coords coords =
|
||||
Coords(int8_t(tileIndex / afterColumnCount), int8_t(tileIndex % afterColumnCount));
|
||||
Coords(static_cast<int8_t>(tileIndex / afterColumnCount),
|
||||
static_cast<int8_t>(tileIndex % afterColumnCount));
|
||||
const auto afterModifier = after.hex_map().terrain(tileIndex).modifier();
|
||||
|
||||
if (tileIndex >= before.hex_map().terrain_size()) {
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace shardok {
|
||||
using net::eagle0::shardok::api::GameStateView;
|
||||
using net::eagle0::shardok::api::GameStateViewDiff;
|
||||
|
||||
auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
|
||||
[[nodiscard]] auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
|
||||
-> std::optional<GameStateViewDiff>;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -57,6 +57,7 @@ cc_library(
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_exception",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
@@ -71,13 +72,13 @@ auto PopulateGameOverResponse(
|
||||
|
||||
// copy in all the resolvedUnits that correspond to this player
|
||||
for (const auto &internalRu : resolvedUnits) {
|
||||
const auto *unit = reinterpret_cast<const Unit *>(internalRu.unit_bytes().data());
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
const auto unit = UnitFromBytes(internalRu.unit_bytes());
|
||||
if (!unit.has_attached_hero()) continue;
|
||||
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
|
||||
if (unit->player_id() != shardokPid) continue;
|
||||
if (unit.player_id() != shardokPid) continue;
|
||||
|
||||
auto *ru = resolutionInfo->add_units();
|
||||
*ru->mutable_unit() = FromUnitFb(*unit);
|
||||
*ru->mutable_unit() = FromUnitFb(unit);
|
||||
ru->set_status(FromInternalStatus(internalRu.status()));
|
||||
if (internalRu.status() ==
|
||||
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_SECURED_UNIT) {
|
||||
|
||||
@@ -22,12 +22,12 @@ auto PopulateGameOverResponse(
|
||||
const std::vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
|
||||
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
|
||||
|
||||
auto IncludeUnitProtoInReturn(
|
||||
[[nodiscard]] auto IncludeUnitProtoInReturn(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
|
||||
|
||||
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
|
||||
[[nodiscard]] auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
|
||||
|
||||
auto FromInternalStatus(
|
||||
[[nodiscard]] auto FromInternalStatus(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
|
||||
-> net::eagle0::common::UnitStatus;
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ private:
|
||||
* Read auth token from a file, stripping whitespace.
|
||||
* Returns empty string if file doesn't exist or is empty.
|
||||
*/
|
||||
inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
|
||||
[[nodiscard]] inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
|
||||
if (filePath.empty()) { return ""; }
|
||||
|
||||
std::ifstream file(filePath);
|
||||
|
||||
@@ -26,10 +26,10 @@ public:
|
||||
[[nodiscard]] auto what() const noexcept -> const char* override { return description.c_str(); }
|
||||
};
|
||||
|
||||
auto LoadMap(const std::string& mapName) -> HexMapProto;
|
||||
[[nodiscard]] auto LoadMap(const std::string& mapName) -> HexMapProto;
|
||||
|
||||
auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
|
||||
[[nodiscard]] auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
|
||||
|
||||
auto GetMapNames() -> std::vector<std::string>;
|
||||
[[nodiscard]] auto GetMapNames() -> std::vector<std::string>;
|
||||
|
||||
#endif // EAGLE0_SHARDOK_UTIL_MAP_LOADER_HPP
|
||||
|
||||
+4
-4
@@ -37,7 +37,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -2, y: 0}
|
||||
m_SizeDelta: {x: -14, y: 0}
|
||||
m_Pivot: {x: 0, y: 1}
|
||||
--- !u!222 &8488118418423736121
|
||||
CanvasRenderer:
|
||||
@@ -1863,7 +1863,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 217.5914, y: -8}
|
||||
m_AnchoredPosition: {x: 330, y: -8}
|
||||
m_SizeDelta: {x: 173.1829, y: 202}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &5247279438940124088
|
||||
@@ -2369,8 +2369,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchoredPosition: {x: -79, y: 0}
|
||||
m_SizeDelta: {x: 395, y: -20}
|
||||
m_AnchoredPosition: {x: -150, y: 0}
|
||||
m_SizeDelta: {x: 560, y: -20}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &531306430670215360
|
||||
MonoBehaviour:
|
||||
|
||||
+423
-9
@@ -20,6 +20,35 @@ namespace eagle {
|
||||
private int _availableGold;
|
||||
private int _maxArmament;
|
||||
private MaxAttainableEverywhereBattalions _maxAttainableEverywhereBattalions;
|
||||
private int? _selectedBattalionIndex;
|
||||
private GameObject _touchActionStrip;
|
||||
private TMP_Text _touchActionStripLabel;
|
||||
private Button _touchPlusButton;
|
||||
private Button _touchMinusButton;
|
||||
private Button _touchResetButton;
|
||||
private Button _touchMaxButton;
|
||||
private Image _touchPlusIcon;
|
||||
private Image _touchMinusIcon;
|
||||
private Image _touchResetIcon;
|
||||
private Image _touchMaxIcon;
|
||||
private Transform _touchActionButtonGrid;
|
||||
private GameObject _allBattalionsHeader;
|
||||
private GameObject _allBattalionsMaxButtonRow;
|
||||
private bool _capturedTableLayout;
|
||||
private bool _capturedAllBattalionsLayout;
|
||||
private Vector2 _tableAnchoredPosition;
|
||||
private Vector2 _tableSizeDelta;
|
||||
private RectTransform _allBattalionsControlsRectTransform;
|
||||
private Vector2 _allBattalionsControlsAnchoredPosition;
|
||||
private Vector2 _allBattalionsControlsSizeDelta;
|
||||
private VerticalLayoutGroup _allBattalionsControlsLayout;
|
||||
private TextAnchor _allBattalionsControlsAlignment;
|
||||
private RectTransform _maxAllButtonRectTransform;
|
||||
private RectTransform _maxEvenButtonRectTransform;
|
||||
private Transform _maxAllButtonParent;
|
||||
private Transform _maxEvenButtonParent;
|
||||
private int _maxAllButtonSiblingIndex;
|
||||
private int _maxEvenButtonSiblingIndex;
|
||||
|
||||
public EventBasedTable armTroopsTable;
|
||||
public TMP_Text totalCostText;
|
||||
@@ -34,6 +63,9 @@ namespace eagle {
|
||||
private ArmTroopsAvailableCommand ArmTroopsAvailableCommand =>
|
||||
_availableCommand.ArmTroopsCommand;
|
||||
private ProvinceId ActingProvinceId => ArmTroopsAvailableCommand.ActingProvinceId;
|
||||
private bool UseTouchActionStrip =>
|
||||
IsNarrowLayout && GameInput.HasTouchscreen() &&
|
||||
(!GameInput.HasMousePointer() || GameInput.HasSimulatedTouchscreen());
|
||||
|
||||
private struct MaxAttainableEverywhereBattalions {
|
||||
public int Armament;
|
||||
@@ -107,6 +139,14 @@ namespace eagle {
|
||||
}
|
||||
|
||||
protected override void SetUpUI() {
|
||||
EnsureTouchActionStrip();
|
||||
if (armTroopsTable.RowClickedHandler == null) {
|
||||
armTroopsTable.RowClickedHandler = new EventBasedTable.RowClickedEvent();
|
||||
}
|
||||
armTroopsTable.WireRuntimeRowClickHandlers = true;
|
||||
armTroopsTable.RowClickedHandler.RemoveListener(RowClicked);
|
||||
armTroopsTable.RowClickedHandler.AddListener(RowClicked);
|
||||
|
||||
var province = _model.Provinces[ActingProvinceId];
|
||||
_battalions =
|
||||
ArmTroopsAvailableCommand.AvailableBattalions
|
||||
@@ -126,16 +166,21 @@ namespace eagle {
|
||||
_maxArmament = ArmTroopsAvailableCommand.MaxArmament;
|
||||
_maxAttainableEverywhereBattalions = SpendRemainingGold(SpendEvenlyOnly());
|
||||
|
||||
if (_maxAttainableEverywhereBattalions.Armament == _maxArmament) {
|
||||
maxEvenButton.gameObject.SetActive(false);
|
||||
} else {
|
||||
maxEvenLabel.text = _maxAttainableEverywhereBattalions.Armament.ToString();
|
||||
maxEvenButton.gameObject.SetActive(true);
|
||||
}
|
||||
maxEvenLabel.text = _maxAttainableEverywhereBattalions.Armament.ToString();
|
||||
maxEvenButton.gameObject.SetActive(true);
|
||||
maxEvenButton.interactable =
|
||||
_maxAttainableEverywhereBattalions.Armament != _maxArmament;
|
||||
|
||||
maxLabel.text = _maxArmament.ToString();
|
||||
availableText.text = _availableGold.ToString();
|
||||
|
||||
ApplyInputModeLayout();
|
||||
UpdateTableAndCost();
|
||||
}
|
||||
|
||||
public override void OnLayoutModeChanged() {
|
||||
if (_battalions == null) return;
|
||||
ApplyInputModeLayout();
|
||||
UpdateTableAndCost();
|
||||
}
|
||||
|
||||
@@ -159,6 +204,11 @@ namespace eagle {
|
||||
row.MinusButtonClickedCallback = () => MinusClicked(index);
|
||||
row.ResetButtonClickedCallback = () => ResetClicked(index);
|
||||
row.MaxButtonClickedCallback = () => MaxClicked(index);
|
||||
row.RowClickedCallback = () => RowClicked(index);
|
||||
row.SetActionButtonsVisible(!UseTouchActionStrip);
|
||||
row.SetContentRaycastTargets(!UseTouchActionStrip);
|
||||
row.Selectable = true;
|
||||
row.Selected = UseTouchActionStrip && _selectedBattalionIndex == index;
|
||||
|
||||
SetValuesAndButtons(index);
|
||||
}
|
||||
@@ -172,12 +222,13 @@ namespace eagle {
|
||||
row.NewArmament = newArmament;
|
||||
row.Name = battalion.Name;
|
||||
|
||||
row.AllowMax = battalion.Armament < _maxArmament;
|
||||
row.AllowPlus = battalion.Armament < _maxArmament;
|
||||
row.AllowMax = newArmament < _maxArmament;
|
||||
row.AllowPlus = newArmament < _maxArmament;
|
||||
row.AllowMinus = battalion.Armament < newArmament;
|
||||
row.AllowReset = battalion.Armament != newArmament;
|
||||
|
||||
UpdateCost();
|
||||
UpdateTouchActionStrip();
|
||||
}
|
||||
|
||||
private void UpdateCost() {
|
||||
@@ -198,11 +249,28 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void UpdateTableAndCost() {
|
||||
if (UseTouchActionStrip && _battalions.Count > 0) {
|
||||
if (_selectedBattalionIndex == null || _selectedBattalionIndex < 0 ||
|
||||
_selectedBattalionIndex >= _battalions.Count) {
|
||||
_selectedBattalionIndex = 0;
|
||||
}
|
||||
} else {
|
||||
_selectedBattalionIndex = null;
|
||||
}
|
||||
|
||||
armTroopsTable.RowCount = _battalions.Count;
|
||||
|
||||
for (int i = 0; i < _battalions.Count; i++) { PopulateRow(i); }
|
||||
armTroopsTable.RefreshRowEventHandlers();
|
||||
|
||||
UpdateCost();
|
||||
UpdateTouchActionStrip();
|
||||
}
|
||||
|
||||
public void RowClicked(int index) {
|
||||
if (!UseTouchActionStrip) return;
|
||||
|
||||
SelectBattalion(index);
|
||||
}
|
||||
|
||||
public void PlusClicked(int index) {
|
||||
@@ -276,5 +344,351 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private int Cost() { return Cost(_newArmaments); }
|
||||
|
||||
private void SelectBattalion(int index) {
|
||||
_selectedBattalionIndex = index;
|
||||
for (int i = 0; i < _battalions.Count; i++) {
|
||||
armTroopsTable.ComponentAt<ArmTroopsRowController>(i).Selected = i == index;
|
||||
}
|
||||
UpdateTouchActionStrip();
|
||||
}
|
||||
|
||||
private bool TrySelectedBattalionIndex(out int index) {
|
||||
index = _selectedBattalionIndex ?? -1;
|
||||
return UseTouchActionStrip && index >= 0 && index < _battalions.Count;
|
||||
}
|
||||
|
||||
private void UpdateTouchActionStrip() {
|
||||
if (_touchActionStrip == null) return;
|
||||
|
||||
if (!TrySelectedBattalionIndex(out int index)) {
|
||||
_touchActionStrip.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var battalion = _battalions[index];
|
||||
var originalArmament = (int)(battalion.Armament ?? 0);
|
||||
var newArmament = _newArmaments[index];
|
||||
|
||||
_touchActionStrip.SetActive(true);
|
||||
_touchActionStripLabel.text = battalion.Name;
|
||||
UpdateTouchActionButtonIcons();
|
||||
_touchPlusButton.interactable = newArmament < _maxArmament;
|
||||
_touchMinusButton.interactable = originalArmament < newArmament;
|
||||
_touchResetButton.interactable = originalArmament != newArmament;
|
||||
_touchMaxButton.interactable = newArmament < _maxArmament;
|
||||
}
|
||||
|
||||
private void EnsureTouchActionStrip() {
|
||||
if (_touchActionStrip != null) return;
|
||||
|
||||
_touchActionStrip = new GameObject(
|
||||
"Touch Action Strip",
|
||||
typeof(RectTransform),
|
||||
typeof(Image),
|
||||
typeof(VerticalLayoutGroup));
|
||||
_touchActionStrip.transform.SetParent(transform, false);
|
||||
|
||||
var rectTransform = _touchActionStrip.GetComponent<RectTransform>();
|
||||
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
||||
rectTransform.anchoredPosition = new Vector2(55f, -16f);
|
||||
rectTransform.sizeDelta = new Vector2(150f, 186f);
|
||||
|
||||
var background = _touchActionStrip.GetComponent<Image>();
|
||||
background.color = Color.clear;
|
||||
|
||||
var layout = _touchActionStrip.GetComponent<VerticalLayoutGroup>();
|
||||
layout.padding = new RectOffset(0, 0, 0, 0);
|
||||
layout.spacing = 6f;
|
||||
layout.childAlignment = TextAnchor.UpperCenter;
|
||||
layout.childControlWidth = true;
|
||||
layout.childControlHeight = true;
|
||||
layout.childForceExpandWidth = false;
|
||||
layout.childForceExpandHeight = false;
|
||||
|
||||
_touchActionStripLabel = CreateActionStripLabel();
|
||||
_touchActionButtonGrid = CreateActionButtonGrid();
|
||||
_touchMinusButton = CreateActionStripButton("Minus Button", out _touchMinusIcon, () => {
|
||||
if (TrySelectedBattalionIndex(out int index)) { MinusClicked(index); }
|
||||
});
|
||||
_touchPlusButton = CreateActionStripButton("Plus Button", out _touchPlusIcon, () => {
|
||||
if (TrySelectedBattalionIndex(out int index)) { PlusClicked(index); }
|
||||
});
|
||||
_touchResetButton = CreateActionStripButton("Reset Button", out _touchResetIcon, () => {
|
||||
if (TrySelectedBattalionIndex(out int index)) { ResetClicked(index); }
|
||||
});
|
||||
_touchMaxButton = CreateActionStripButton("Max Button", out _touchMaxIcon, () => {
|
||||
if (TrySelectedBattalionIndex(out int index)) { MaxClicked(index); }
|
||||
});
|
||||
|
||||
_touchActionStrip.SetActive(false);
|
||||
EnsureAllBattalionsHeader();
|
||||
}
|
||||
|
||||
private void ApplyInputModeLayout() {
|
||||
var tableRectTransform = armTroopsTable.GetComponent<RectTransform>();
|
||||
if (!_capturedTableLayout) {
|
||||
_tableAnchoredPosition = tableRectTransform.anchoredPosition;
|
||||
_tableSizeDelta = tableRectTransform.sizeDelta;
|
||||
_allBattalionsControlsRectTransform =
|
||||
maxEvenButton.transform.parent.GetComponent<RectTransform>();
|
||||
_allBattalionsControlsAnchoredPosition =
|
||||
_allBattalionsControlsRectTransform.anchoredPosition;
|
||||
_allBattalionsControlsSizeDelta = _allBattalionsControlsRectTransform.sizeDelta;
|
||||
_capturedTableLayout = true;
|
||||
}
|
||||
CaptureAllBattalionsLayout();
|
||||
EnsureAllBattalionsHeader();
|
||||
EnsureAllBattalionsMaxButtonRow();
|
||||
|
||||
if (UseTouchActionStrip) {
|
||||
var leftEdge = _tableAnchoredPosition.x - _tableSizeDelta.x / 2f;
|
||||
var touchTableWidth = 270f;
|
||||
tableRectTransform.anchoredPosition =
|
||||
new Vector2(leftEdge + touchTableWidth / 2f, _tableAnchoredPosition.y);
|
||||
tableRectTransform.sizeDelta = new Vector2(touchTableWidth, _tableSizeDelta.y);
|
||||
_allBattalionsControlsRectTransform.anchoredPosition =
|
||||
_allBattalionsControlsAnchoredPosition;
|
||||
_allBattalionsControlsRectTransform.sizeDelta = new Vector2(190f, 202f);
|
||||
_allBattalionsControlsLayout.childAlignment = TextAnchor.UpperCenter;
|
||||
|
||||
var touchActionStripRectTransform = _touchActionStrip.GetComponent<RectTransform>();
|
||||
touchActionStripRectTransform.anchoredPosition = new Vector2(
|
||||
touchActionStripRectTransform.anchoredPosition.x,
|
||||
_allBattalionsControlsAnchoredPosition.y);
|
||||
touchActionStripRectTransform.sizeDelta =
|
||||
new Vector2(touchActionStripRectTransform.sizeDelta.x, 202f);
|
||||
|
||||
_allBattalionsHeader.transform.SetParent(
|
||||
_allBattalionsControlsRectTransform,
|
||||
false);
|
||||
_allBattalionsHeader.transform.SetSiblingIndex(0);
|
||||
_allBattalionsMaxButtonRow.transform.SetParent(
|
||||
_allBattalionsControlsRectTransform,
|
||||
false);
|
||||
_allBattalionsMaxButtonRow.transform.SetSiblingIndex(1);
|
||||
_maxAllButtonRectTransform.SetParent(_allBattalionsMaxButtonRow.transform, false);
|
||||
_maxAllButtonRectTransform.SetSiblingIndex(0);
|
||||
_maxEvenButtonRectTransform.SetParent(_allBattalionsMaxButtonRow.transform, false);
|
||||
_maxEvenButtonRectTransform.SetSiblingIndex(1);
|
||||
|
||||
_allBattalionsHeader.SetActive(true);
|
||||
_allBattalionsMaxButtonRow.SetActive(true);
|
||||
} else {
|
||||
tableRectTransform.anchoredPosition = _tableAnchoredPosition;
|
||||
tableRectTransform.sizeDelta = _tableSizeDelta;
|
||||
_allBattalionsControlsRectTransform.anchoredPosition =
|
||||
_allBattalionsControlsAnchoredPosition;
|
||||
_allBattalionsControlsRectTransform.sizeDelta = _allBattalionsControlsSizeDelta;
|
||||
_allBattalionsControlsLayout.childAlignment = _allBattalionsControlsAlignment;
|
||||
RestoreAllBattalionsMaxButtons();
|
||||
if (_allBattalionsHeader != null) { _allBattalionsHeader.SetActive(false); }
|
||||
if (_allBattalionsMaxButtonRow != null) {
|
||||
_allBattalionsMaxButtonRow.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureAllBattalionsLayout() {
|
||||
if (_capturedAllBattalionsLayout) return;
|
||||
|
||||
_allBattalionsControlsLayout =
|
||||
_allBattalionsControlsRectTransform.GetComponent<VerticalLayoutGroup>();
|
||||
_allBattalionsControlsAlignment = _allBattalionsControlsLayout.childAlignment;
|
||||
_maxAllButtonRectTransform =
|
||||
_allBattalionsControlsRectTransform.GetChild(0).GetComponent<RectTransform>();
|
||||
_maxEvenButtonRectTransform = maxEvenButton.GetComponent<RectTransform>();
|
||||
_maxAllButtonParent = _maxAllButtonRectTransform.parent;
|
||||
_maxEvenButtonParent = _maxEvenButtonRectTransform.parent;
|
||||
_maxAllButtonSiblingIndex = _maxAllButtonRectTransform.GetSiblingIndex();
|
||||
_maxEvenButtonSiblingIndex = _maxEvenButtonRectTransform.GetSiblingIndex();
|
||||
|
||||
_capturedAllBattalionsLayout = true;
|
||||
}
|
||||
|
||||
private void RestoreAllBattalionsMaxButtons() {
|
||||
_maxAllButtonRectTransform.SetParent(_maxAllButtonParent, false);
|
||||
_maxAllButtonRectTransform.SetSiblingIndex(_maxAllButtonSiblingIndex);
|
||||
_maxEvenButtonRectTransform.SetParent(_maxEvenButtonParent, false);
|
||||
_maxEvenButtonRectTransform.SetSiblingIndex(_maxEvenButtonSiblingIndex);
|
||||
}
|
||||
|
||||
private TMP_Text CreateActionStripLabel() {
|
||||
var label = new GameObject(
|
||||
"Selected Battalion Label",
|
||||
typeof(RectTransform),
|
||||
typeof(LayoutElement),
|
||||
typeof(TextMeshProUGUI));
|
||||
label.transform.SetParent(_touchActionStrip.transform, false);
|
||||
|
||||
var layoutElement = label.GetComponent<LayoutElement>();
|
||||
layoutElement.minWidth = 150f;
|
||||
layoutElement.preferredWidth = 150f;
|
||||
layoutElement.preferredHeight = 44f;
|
||||
|
||||
var text = label.GetComponent<TextMeshProUGUI>();
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.color = Color.black;
|
||||
text.font = maxLabel.font;
|
||||
text.fontSize = 18f;
|
||||
text.enableWordWrapping = true;
|
||||
text.overflowMode = TextOverflowModes.Overflow;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private Transform CreateActionButtonGrid() {
|
||||
var gridObject = new GameObject(
|
||||
"Action Button Grid",
|
||||
typeof(RectTransform),
|
||||
typeof(LayoutElement),
|
||||
typeof(GridLayoutGroup));
|
||||
gridObject.transform.SetParent(_touchActionStrip.transform, false);
|
||||
|
||||
var layoutElement = gridObject.GetComponent<LayoutElement>();
|
||||
layoutElement.minWidth = 108f;
|
||||
layoutElement.preferredWidth = 108f;
|
||||
layoutElement.minHeight = 108f;
|
||||
layoutElement.preferredHeight = 108f;
|
||||
|
||||
var grid = gridObject.GetComponent<GridLayoutGroup>();
|
||||
grid.cellSize = new Vector2(48f, 48f);
|
||||
grid.spacing = new Vector2(6f, 6f);
|
||||
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
||||
grid.constraintCount = 2;
|
||||
grid.childAlignment = TextAnchor.MiddleCenter;
|
||||
|
||||
return gridObject.transform;
|
||||
}
|
||||
|
||||
private Button CreateActionStripButton(
|
||||
string name,
|
||||
out Image icon,
|
||||
UnityEngine.Events.UnityAction action) {
|
||||
var buttonObject = new GameObject(
|
||||
name,
|
||||
typeof(RectTransform),
|
||||
typeof(LayoutElement),
|
||||
typeof(Image),
|
||||
typeof(Button));
|
||||
buttonObject.transform.SetParent(_touchActionButtonGrid, false);
|
||||
|
||||
var layoutElement = buttonObject.GetComponent<LayoutElement>();
|
||||
layoutElement.minWidth = 48f;
|
||||
layoutElement.preferredWidth = 48f;
|
||||
layoutElement.minHeight = 48f;
|
||||
layoutElement.preferredHeight = 48f;
|
||||
|
||||
var image = buttonObject.GetComponent<Image>();
|
||||
image.color = new Color(0.94f, 0.94f, 0.96f, 1f);
|
||||
CopyButtonBackground(maxEvenButton, image);
|
||||
|
||||
var button = buttonObject.GetComponent<Button>();
|
||||
button.targetGraphic = image;
|
||||
button.onClick.AddListener(action);
|
||||
|
||||
var iconObject = new GameObject("Icon", typeof(RectTransform), typeof(Image));
|
||||
iconObject.transform.SetParent(buttonObject.transform, false);
|
||||
|
||||
var iconRectTransform = iconObject.GetComponent<RectTransform>();
|
||||
iconRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
iconRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
iconRectTransform.anchoredPosition = Vector2.zero;
|
||||
iconRectTransform.sizeDelta = new Vector2(34f, 34f);
|
||||
|
||||
icon = iconObject.GetComponent<Image>();
|
||||
icon.color = Color.black;
|
||||
icon.preserveAspect = true;
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void UpdateTouchActionButtonIcons() {
|
||||
if (_battalions == null || _battalions.Count == 0) return;
|
||||
|
||||
var row = armTroopsTable.ComponentAt<ArmTroopsRowController>(0);
|
||||
_touchMinusIcon.sprite = ButtonIconSprite(row.minusButton);
|
||||
_touchPlusIcon.sprite = ButtonIconSprite(row.plusButton);
|
||||
_touchResetIcon.sprite = ButtonIconSprite(row.resetButton);
|
||||
_touchMaxIcon.sprite = ButtonIconSprite(row.maxButton);
|
||||
}
|
||||
|
||||
private Sprite ButtonIconSprite(Button button) {
|
||||
foreach (var image in button.GetComponentsInChildren<Image>(includeInactive: true)) {
|
||||
if (image.gameObject != button.gameObject && image.sprite != null) {
|
||||
return image.sprite;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CopyButtonBackground(Button sourceButton, Image targetImage) {
|
||||
var sourceImage = sourceButton.targetGraphic as Image;
|
||||
if (sourceImage == null) return;
|
||||
|
||||
targetImage.sprite = sourceImage.sprite;
|
||||
targetImage.type = sourceImage.type;
|
||||
targetImage.pixelsPerUnitMultiplier = sourceImage.pixelsPerUnitMultiplier;
|
||||
}
|
||||
|
||||
private void EnsureAllBattalionsMaxButtonRow() {
|
||||
if (_allBattalionsMaxButtonRow != null) return;
|
||||
|
||||
_allBattalionsMaxButtonRow = new GameObject(
|
||||
"All Battalions Max Button Row",
|
||||
typeof(RectTransform),
|
||||
typeof(LayoutElement),
|
||||
typeof(HorizontalLayoutGroup));
|
||||
_allBattalionsMaxButtonRow.transform.SetParent(
|
||||
_allBattalionsControlsRectTransform,
|
||||
false);
|
||||
|
||||
var layoutElement = _allBattalionsMaxButtonRow.GetComponent<LayoutElement>();
|
||||
layoutElement.minWidth = 190f;
|
||||
layoutElement.preferredWidth = 190f;
|
||||
layoutElement.minHeight = 60f;
|
||||
layoutElement.preferredHeight = 60f;
|
||||
|
||||
var layout = _allBattalionsMaxButtonRow.GetComponent<HorizontalLayoutGroup>();
|
||||
layout.padding = new RectOffset(0, 0, 0, 0);
|
||||
layout.spacing = 8f;
|
||||
layout.childAlignment = TextAnchor.MiddleCenter;
|
||||
layout.childControlWidth = true;
|
||||
layout.childControlHeight = true;
|
||||
layout.childForceExpandWidth = false;
|
||||
layout.childForceExpandHeight = false;
|
||||
|
||||
_allBattalionsMaxButtonRow.SetActive(false);
|
||||
}
|
||||
|
||||
private void EnsureAllBattalionsHeader() {
|
||||
if (_allBattalionsHeader != null) return;
|
||||
|
||||
_allBattalionsHeader = new GameObject(
|
||||
"All Battalions Header",
|
||||
typeof(RectTransform),
|
||||
typeof(LayoutElement),
|
||||
typeof(TextMeshProUGUI));
|
||||
_allBattalionsHeader.transform.SetParent(transform, false);
|
||||
|
||||
var layoutElement = _allBattalionsHeader.GetComponent<LayoutElement>();
|
||||
layoutElement.minWidth = 190f;
|
||||
layoutElement.preferredWidth = 190f;
|
||||
layoutElement.minHeight = 44f;
|
||||
layoutElement.preferredHeight = 44f;
|
||||
|
||||
var text = _allBattalionsHeader.GetComponent<TextMeshProUGUI>();
|
||||
text.text = "All Battalions";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.color = Color.black;
|
||||
text.font = maxLabel.font;
|
||||
text.fontSize = 18f;
|
||||
text.enableWordWrapping = false;
|
||||
|
||||
_allBattalionsHeader.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,12 +67,7 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
void Awake() {
|
||||
TouchTargetUtils.EnsureMinimumTouchTarget(dismissAllButton);
|
||||
TouchTargetUtils.EnsureMinimumTouchTarget(
|
||||
dismissOneButtonText.GetComponentInParent<Button>());
|
||||
HideBeastVisual();
|
||||
}
|
||||
void Awake() { HideBeastVisual(); }
|
||||
|
||||
void OnDestroy() {
|
||||
_isDestroying = true;
|
||||
|
||||
+27
-27
@@ -99,7 +99,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
m_Padding:
|
||||
m_Left: 5
|
||||
m_Right: 5
|
||||
m_Right: 8
|
||||
m_Top: 5
|
||||
m_Bottom: 5
|
||||
m_ChildAlignment: 3
|
||||
@@ -124,9 +124,9 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinWidth: 540
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredWidth: 540
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: 1
|
||||
@@ -633,9 +633,9 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 207
|
||||
m_MinWidth: 320
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 207
|
||||
m_PreferredWidth: 320
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
@@ -922,9 +922,9 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 207
|
||||
m_MinWidth: 320
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 207
|
||||
m_PreferredWidth: 320
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
@@ -1149,7 +1149,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 25, y: 25}
|
||||
m_SizeDelta: {x: 26, y: 26}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3462703612756757987
|
||||
CanvasRenderer:
|
||||
@@ -1225,7 +1225,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 25, y: 25}
|
||||
m_SizeDelta: {x: 26, y: 26}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7037872268356765622
|
||||
CanvasRenderer:
|
||||
@@ -1510,10 +1510,10 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 25
|
||||
m_PreferredHeight: 25
|
||||
m_MinWidth: 36
|
||||
m_MinHeight: 36
|
||||
m_PreferredWidth: 36
|
||||
m_PreferredHeight: 36
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
@@ -1553,7 +1553,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 25, y: 25}
|
||||
m_SizeDelta: {x: 26, y: 26}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4669985286182351739
|
||||
CanvasRenderer:
|
||||
@@ -1629,7 +1629,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 25, y: 25}
|
||||
m_SizeDelta: {x: 26, y: 26}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2353172733458224956
|
||||
CanvasRenderer:
|
||||
@@ -1862,10 +1862,10 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 25
|
||||
m_PreferredHeight: 25
|
||||
m_MinWidth: 36
|
||||
m_MinHeight: 36
|
||||
m_PreferredWidth: 36
|
||||
m_PreferredHeight: 36
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
@@ -2017,10 +2017,10 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 25
|
||||
m_PreferredHeight: 25
|
||||
m_MinWidth: 36
|
||||
m_MinHeight: 36
|
||||
m_PreferredWidth: 36
|
||||
m_PreferredHeight: 36
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
@@ -2266,10 +2266,10 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 25
|
||||
m_PreferredHeight: 25
|
||||
m_MinWidth: 36
|
||||
m_MinHeight: 36
|
||||
m_PreferredWidth: 36
|
||||
m_PreferredHeight: 36
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
|
||||
+30
-9
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using static UnityEngine.EventSystems.PointerEventData;
|
||||
|
||||
namespace eagle {
|
||||
public class ArmTroopsRowController : TableRowController {
|
||||
public class ArmTroopsRowController : TableRowController, IPointerClickHandler {
|
||||
private static readonly Color SelectedBackgroundColor = new Color(0.68f, 0.84f, 0.96f, 1f);
|
||||
|
||||
public RawImage battalionImage;
|
||||
public TMP_Text oldArmamentLabel;
|
||||
public TMP_Text newArmamentLabel;
|
||||
@@ -21,16 +25,10 @@ namespace eagle {
|
||||
public Action MinusButtonClickedCallback;
|
||||
public Action ResetButtonClickedCallback;
|
||||
public Action MaxButtonClickedCallback;
|
||||
public Action RowClickedCallback;
|
||||
|
||||
public Texture BattalionImage { set => battalionImage.texture = value; }
|
||||
|
||||
private void Awake() {
|
||||
TouchTargetUtils.EnsureMinimumTouchTarget(plusButton);
|
||||
TouchTargetUtils.EnsureMinimumTouchTarget(minusButton);
|
||||
TouchTargetUtils.EnsureMinimumTouchTarget(resetButton);
|
||||
TouchTargetUtils.EnsureMinimumTouchTarget(maxButton);
|
||||
}
|
||||
|
||||
public double CostPerPoint { set => costPerPointLabel.text = value.ToString("0.##"); }
|
||||
|
||||
public double OldArmament { set => oldArmamentLabel.text = Math.Floor(value).ToString(); }
|
||||
@@ -49,6 +47,29 @@ namespace eagle {
|
||||
|
||||
public bool AllowMax { set => maxButton.interactable = value; }
|
||||
|
||||
public void SetActionButtonsVisible(bool visible) {
|
||||
plusButton.gameObject.SetActive(visible);
|
||||
minusButton.gameObject.SetActive(visible);
|
||||
resetButton.gameObject.SetActive(visible);
|
||||
maxButton.gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
public void SetContentRaycastTargets(bool enabled) {
|
||||
foreach (var graphic in GetComponentsInChildren<Graphic>()) {
|
||||
if (graphic.gameObject == gameObject) continue;
|
||||
graphic.raycastTarget = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ColorRow() {
|
||||
base.ColorRow();
|
||||
if (Selected) { gameObject.GetComponent<Image>().color = SelectedBackgroundColor; }
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData) {
|
||||
if (eventData.button == InputButton.Left) { RowClickedCallback?.Invoke(); }
|
||||
}
|
||||
|
||||
public void PlusButtonClicked() { PlusButtonClickedCallback(); }
|
||||
|
||||
public void MinusButtonClicked() { MinusButtonClickedCallback(); }
|
||||
|
||||
@@ -61420,8 +61420,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 67.5, y: -17.5}
|
||||
m_SizeDelta: {x: 125, y: 35}
|
||||
m_AnchoredPosition: {x: 67.5, y: -24}
|
||||
m_SizeDelta: {x: 125, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &448680368
|
||||
MonoBehaviour:
|
||||
@@ -61437,9 +61437,9 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 125
|
||||
m_MinHeight: 35
|
||||
m_MinHeight: 48
|
||||
m_PreferredWidth: 125
|
||||
m_PreferredHeight: 35
|
||||
m_PreferredHeight: 48
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
@@ -81276,8 +81276,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchoredPosition: {x: -79, y: 0}
|
||||
m_SizeDelta: {x: 395, y: -20}
|
||||
m_AnchoredPosition: {x: -96.5, y: 0}
|
||||
m_SizeDelta: {x: 430, y: -20}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &574323926
|
||||
GameObject:
|
||||
@@ -85441,7 +85441,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -702}
|
||||
m_SizeDelta: {x: 0, y: 324}
|
||||
m_SizeDelta: {x: 0, y: 288}
|
||||
m_Pivot: {x: 1, y: 1}
|
||||
--- !u!114 &604968800
|
||||
MonoBehaviour:
|
||||
@@ -127452,8 +127452,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 650, y: -265.5}
|
||||
m_SizeDelta: {x: 1290, y: 35}
|
||||
m_AnchoredPosition: {x: 650, y: -264}
|
||||
m_SizeDelta: {x: 1290, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &897090470
|
||||
MonoBehaviour:
|
||||
@@ -180946,8 +180946,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 580, y: -17.5}
|
||||
m_SizeDelta: {x: 890, y: 35}
|
||||
m_AnchoredPosition: {x: 580, y: -24}
|
||||
m_SizeDelta: {x: 890, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1305016253
|
||||
MonoBehaviour:
|
||||
@@ -224378,8 +224378,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 1092.5, y: -17.5}
|
||||
m_SizeDelta: {x: 125, y: 35}
|
||||
m_AnchoredPosition: {x: 1092.5, y: -24}
|
||||
m_SizeDelta: {x: 125, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1601044727
|
||||
MonoBehaviour:
|
||||
@@ -224395,9 +224395,9 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 125
|
||||
m_MinHeight: 35
|
||||
m_MinHeight: 48
|
||||
m_PreferredWidth: 125
|
||||
m_PreferredHeight: 35
|
||||
m_PreferredHeight: 48
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
@@ -240119,7 +240119,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -2, y: 0}
|
||||
m_SizeDelta: {x: -14, y: 0}
|
||||
m_Pivot: {x: 0, y: 1}
|
||||
--- !u!114 &1703297659
|
||||
MonoBehaviour:
|
||||
@@ -249518,8 +249518,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 1222.5, y: -17.5}
|
||||
m_SizeDelta: {x: 125, y: 35}
|
||||
m_AnchoredPosition: {x: 1222.5, y: -24}
|
||||
m_SizeDelta: {x: 125, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1769007338
|
||||
MonoBehaviour:
|
||||
@@ -249535,9 +249535,9 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 125
|
||||
m_MinHeight: 35
|
||||
m_MinHeight: 48
|
||||
m_PreferredWidth: 125
|
||||
m_PreferredHeight: 35
|
||||
m_PreferredHeight: 48
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 0
|
||||
m_LayoutPriority: 1
|
||||
|
||||
+10
-1
@@ -1747,7 +1747,16 @@ namespace Shardok {
|
||||
SetDisplayedCommandGroup(0);
|
||||
}
|
||||
_selectedGridIndex = selectedIndex;
|
||||
RedrawCommandOverlays(selectedIndex, selectedIndex);
|
||||
if (Model.GameStatus != null &&
|
||||
Model.GameStatus.State == GameStatus.Types.State.GameRunning &&
|
||||
CommandsForSelectedCoords.Any()) {
|
||||
SelectPreferredCommandGroupForSelectedUnit();
|
||||
if (_displayedCommandGroup == 0) {
|
||||
RedrawCommandOverlays(selectedIndex, selectedIndex);
|
||||
}
|
||||
} else {
|
||||
RedrawCommandOverlays(selectedIndex, selectedIndex);
|
||||
}
|
||||
|
||||
// Notify tutorial system of unit selection
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnUnitSelected(selectedIndex);
|
||||
|
||||
+9
-3
@@ -27,6 +27,7 @@ namespace common {
|
||||
public RowClickedEvent RowRightClickedHandler;
|
||||
public HoverRowChangedEvent HoverRowChangedHandler;
|
||||
public HoverRowChangedEvent LongHoverRowChangedHandler;
|
||||
public bool WireRuntimeRowClickHandlers;
|
||||
|
||||
private Color _evenColor;
|
||||
private Color _oddColor;
|
||||
@@ -71,6 +72,10 @@ namespace common {
|
||||
|
||||
public T AddRowWithComponent<T>() { return AddRow().GetComponent<T>(); }
|
||||
|
||||
public void RefreshRowEventHandlers() {
|
||||
foreach (var row in _rows) { SetUpClickDetector(row); }
|
||||
}
|
||||
|
||||
private void ColorRows() {
|
||||
if (ShadeAlternateRows) {
|
||||
_rows.Each((row, idx) => {
|
||||
@@ -94,12 +99,13 @@ namespace common {
|
||||
clickDetector.onRightUp.RemoveAllListeners();
|
||||
}
|
||||
|
||||
if (RowClickedHandler != null && RowClickedHandler.GetPersistentEventCount() > 0) {
|
||||
if (RowClickedHandler != null && clickDetector != null &&
|
||||
(WireRuntimeRowClickHandlers || RowClickedHandler.GetPersistentEventCount() > 0)) {
|
||||
clickDetector.onClick.AddListener(() => RowClicked(row));
|
||||
clickDetector.onLeftDown.AddListener(() => RowLeftDown(row));
|
||||
clickDetector.onLeftUp.AddListener(() => RowLeftUp(row));
|
||||
}
|
||||
if (RowRightClickedHandler != null &&
|
||||
if (RowRightClickedHandler != null && clickDetector != null &&
|
||||
RowRightClickedHandler.GetPersistentEventCount() > 0) {
|
||||
clickDetector.onRightClick.AddListener(() => RowRightClicked(row));
|
||||
clickDetector.onRightDown.AddListener(() => RowRightDown(row));
|
||||
@@ -247,4 +253,4 @@ namespace common {
|
||||
ColorRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using UnityEngine;
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.EnhancedTouch;
|
||||
#endif
|
||||
|
||||
public static class GameInput {
|
||||
@@ -39,6 +40,31 @@ public static class GameInput {
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool HasTouchscreen() {
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Touchscreen.current != null;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool HasMousePointer() {
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Mouse.current != null;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool HasSimulatedTouchscreen() {
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return TouchSimulation.instance != null &&
|
||||
TouchSimulation.instance.simulatedTouchscreen != null;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool PrimaryPointerPressedThisFrame() {
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
|
||||
|
||||
@@ -50,6 +50,7 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:client_text_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:proto_parser",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
@@ -100,6 +101,7 @@ scala_library(
|
||||
":text_generation_result",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:jfr_events",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
@@ -125,6 +127,7 @@ scala_library(
|
||||
":text_generation_result",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:jfr_events",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.util.regex.Pattern
|
||||
import scala.io.Source
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.common.ProtoParser
|
||||
import net.eagle0.common.{ProtoParser, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
import net.eagle0.eagle.internal.client_text.{IncompleteText, IncompleteTextStore, UnrequestedText}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
@@ -43,10 +43,10 @@ case class ClientTextStoreImpl(
|
||||
)
|
||||
|
||||
if completeTexts.contains(id) then {
|
||||
println(s"Text with id $id already exists")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Text with id $id already exists")
|
||||
this
|
||||
} else if incompleteTexts.contains(id) then {
|
||||
println(s"Incomplete text with id $id already exists")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Incomplete text with id $id already exists")
|
||||
this
|
||||
} else {
|
||||
copy(
|
||||
@@ -213,7 +213,7 @@ case class ClientTextStoreImpl(
|
||||
)
|
||||
}
|
||||
.getOrElse {
|
||||
println(
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Warning: Attempted to move non-incomplete text $id back to unrequested"
|
||||
)
|
||||
this
|
||||
@@ -399,7 +399,7 @@ object ClientTextStoreImpl {
|
||||
}
|
||||
.recover {
|
||||
case _: FileNotFoundException =>
|
||||
println("No complete text store found")
|
||||
SimpleTimedLogger.printLogger.logLine("No complete text store found")
|
||||
Vector.empty
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ object ClientTextStoreImpl {
|
||||
}
|
||||
.recover {
|
||||
case _: FileNotFoundException =>
|
||||
println("No incomplete text store found")
|
||||
SimpleTimedLogger.printLogger.logLine("No incomplete text store found")
|
||||
(Vector.empty, Vector.empty)
|
||||
}
|
||||
|
||||
@@ -474,7 +474,7 @@ object ClientTextStoreImpl {
|
||||
Some(id -> accessibleToString.split(",").map(_.toInt).toVector)
|
||||
|
||||
case x =>
|
||||
println(s"Unexpected entry text: ${x.mkString(", ")}")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Unexpected entry text: ${x.mkString(", ")}")
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -482,7 +482,7 @@ object ClientTextStoreImpl {
|
||||
}
|
||||
.recover {
|
||||
case _: FileNotFoundException =>
|
||||
println("No accessibleTo store found")
|
||||
SimpleTimedLogger.printLogger.logLine("No accessibleTo store found")
|
||||
Map.empty
|
||||
|
||||
case x => throw x
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.sql.{Connection, DriverManager, ResultSet}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.common.JfrEvents
|
||||
import net.eagle0.common.{JfrEvents, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId, GameId}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
@@ -200,7 +200,7 @@ class PostgresClientTextStore private[client_text] (
|
||||
)
|
||||
|
||||
if textExists(id) then {
|
||||
println(s"Text with id $id already exists")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Text with id $id already exists")
|
||||
this
|
||||
} else {
|
||||
val sql =
|
||||
@@ -407,10 +407,14 @@ class PostgresClientTextStore private[client_text] (
|
||||
override def withMovedBackToUnrequested(id: ClientTextId): ClientTextStore = {
|
||||
val status = statusFor(id)
|
||||
if status.isEmpty then {
|
||||
println(s"Warning: Attempted to move non-existent text $id back to unrequested")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Warning: Attempted to move non-existent text $id back to unrequested"
|
||||
)
|
||||
this
|
||||
} else if !status.contains("incomplete") then {
|
||||
println(s"Warning: Attempted to move non-incomplete text $id back to unrequested")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Warning: Attempted to move non-incomplete text $id back to unrequested"
|
||||
)
|
||||
this
|
||||
} else {
|
||||
val sql =
|
||||
@@ -449,7 +453,7 @@ class PostgresClientTextStore private[client_text] (
|
||||
|
||||
if ids.isEmpty then this
|
||||
else {
|
||||
println(
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Removing ${ids.size} generated text row(s) after rewound history count $maxHistoryCount: ${ids.mkString(", ")}"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.sql.{Connection, DriverManager}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.common.JfrEvents
|
||||
import net.eagle0.common.{JfrEvents, SimpleTimedLogger}
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
@@ -238,7 +238,7 @@ class SqliteClientTextStore private[client_text] (
|
||||
|
||||
existingStatus match {
|
||||
case Some(status) =>
|
||||
println(s"Text with id $id already exists (status=$status)")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Text with id $id already exists (status=$status)")
|
||||
this
|
||||
case None =>
|
||||
// Insert new text as unrequested
|
||||
@@ -493,10 +493,14 @@ class SqliteClientTextStore private[client_text] (
|
||||
|
||||
status match {
|
||||
case None =>
|
||||
println(s"Warning: Attempted to move non-existent text $id back to unrequested")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Warning: Attempted to move non-existent text $id back to unrequested"
|
||||
)
|
||||
this
|
||||
case Some(otherStatus) if otherStatus != "incomplete" =>
|
||||
println(s"Warning: Attempted to move non-incomplete text $id back to unrequested")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Warning: Attempted to move non-incomplete text $id back to unrequested"
|
||||
)
|
||||
this
|
||||
case Some(_) =>
|
||||
// Update to unrequested, reset text
|
||||
@@ -529,7 +533,7 @@ class SqliteClientTextStore private[client_text] (
|
||||
|
||||
if ids.isEmpty then this
|
||||
else {
|
||||
println(
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Removing ${ids.size} generated text row(s) after rewound history count $maxHistoryCount: ${ids.mkString(", ")}"
|
||||
)
|
||||
|
||||
@@ -903,7 +907,9 @@ object SqliteClientTextStore {
|
||||
}
|
||||
java.nio.file.Files.copy(inputStream, dbFile.toPath)
|
||||
inputStream.close()
|
||||
println(s"Downloaded SQLite database from cloud storage: ${dbFile.getAbsolutePath}")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Downloaded SQLite database from cloud storage: ${dbFile.getAbsolutePath}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
import net.eagle0.common.SimpleTimedLogger
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.view_filters.GameStateViewFilter
|
||||
@@ -120,8 +121,10 @@ object ActionResultFilter {
|
||||
} &&
|
||||
!allowOpponentActionPhases.contains(startingState.currentPhase)
|
||||
then {
|
||||
println("Learned about somebody else's action!")
|
||||
println(actionResult)
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Faction $factionId learned about opponent action ${actionResult.actionResultType} " +
|
||||
s"from ${actionResult.actingFactionId} during ${startingState.currentPhase}"
|
||||
)
|
||||
}
|
||||
Option.when(includeForPlayer(factionId, result) || gsDiff.nonEmpty) {
|
||||
val filteredNotifications = actionResult.newNotifications
|
||||
@@ -200,8 +203,10 @@ object ActionResultFilter {
|
||||
) &&
|
||||
!allowOpponentActionPhases.contains(startingState.currentPhase)
|
||||
then {
|
||||
println("Learned about somebody else's action!")
|
||||
println(actionResult)
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Faction $factionId learned about opponent action ${actionResult.actionResultType} " +
|
||||
s"from ${actionResult.actingFactionId} during ${startingState.currentPhase}"
|
||||
)
|
||||
}
|
||||
|
||||
Option.when(includeForPlayer(Some(factionId), result) || gsDiff.nonEmpty) {
|
||||
|
||||
@@ -55,6 +55,7 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":action_result_filter",
|
||||
":eagle_client_exception",
|
||||
":engine",
|
||||
":game_history",
|
||||
":phase_advancement_result",
|
||||
@@ -102,6 +103,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/action_result:action_result_view",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:game_state_view_differ",
|
||||
|
||||
@@ -338,8 +338,9 @@ case class EngineImpl(
|
||||
val scalaAvailableCommand = scalaAvailableCommandOpt match {
|
||||
case Some(scalaAvailableCommand) => scalaAvailableCommand
|
||||
case None =>
|
||||
commandRequire(false, s"No matching available command for selected command $selectedCommand")
|
||||
throw new IllegalStateException("unreachable")
|
||||
throw new EagleCommandException(
|
||||
s"requirement failed: No matching available command for selected command $selectedCommand"
|
||||
)
|
||||
}
|
||||
|
||||
val commandType = selectedCommand.commandType
|
||||
@@ -360,11 +361,6 @@ case class EngineImpl(
|
||||
|
||||
// Validate that the first result has an acting faction set (required for player commands)
|
||||
val firstResult = sequencer.actionResults.newValue.headOption
|
||||
if !firstResult.forall(_.actingFactionId.isDefined) then {
|
||||
print(
|
||||
"Result with type " + firstResult.map(_.actionResultType).getOrElse("unknown") + " did not have a player set"
|
||||
)
|
||||
}
|
||||
internalRequire(
|
||||
firstResult.forall(_.actingFactionId.isDefined),
|
||||
s"Result with type ${firstResult.map(_.actionResultType).getOrElse("unknown")} did not have a player set"
|
||||
|
||||
@@ -128,6 +128,7 @@ case class ActionResultC(
|
||||
newBattalionTypes = newBattalionTypes,
|
||||
destroyedBattalionIds = destroyedBattalionIds,
|
||||
removedHeroIds = removedHeroes,
|
||||
removedFactionIds = removedFactionIds,
|
||||
clientTextVisibilityExtensions = clientTextVisibilityExtensions,
|
||||
newGeneratedTextRequests = newGeneratedTextRequests,
|
||||
newNotifications = newNotifications,
|
||||
|
||||
@@ -14,6 +14,7 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/model/action_result/concrete:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
|
||||
|
||||
+17
-14
@@ -200,22 +200,10 @@ object SelectedCommandConverter {
|
||||
)
|
||||
|
||||
case cmd: proto.ResolveRansomOfferSelectedCommand =>
|
||||
val prisonerHeroId = cmd.ransomOffer.flatMap { offer =>
|
||||
offer.offerDetails match {
|
||||
case net.eagle0.eagle.common.diplomacy_offer.RansomOfferDetails(
|
||||
prisonerToBeRansomed,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_
|
||||
) =>
|
||||
prisonerToBeRansomed.map(_.prisonerHeroId)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
val prisonerHeroId = prisonerHeroIdFromRansomOffer(cmd.ransomOffer)
|
||||
ResolveRansomOfferSelected(
|
||||
offeringFactionId = cmd.offeringFactionId,
|
||||
prisonerHeroId = prisonerHeroId.getOrElse(-1),
|
||||
prisonerHeroId = prisonerHeroId,
|
||||
resolution = statusFromProto(cmd.resolution)
|
||||
)
|
||||
|
||||
@@ -398,6 +386,21 @@ object SelectedCommandConverter {
|
||||
goldOffered = details.goldOffered
|
||||
)
|
||||
|
||||
private def prisonerHeroIdFromRansomOffer(
|
||||
offer: Option[net.eagle0.eagle.common.diplomacy_offer.DiplomacyOffer]
|
||||
): Int = {
|
||||
val ransomOffer = offer.getOrElse(
|
||||
throw new ProtoConversionException("ResolveRansomOfferSelectedCommand missing ransomOffer")
|
||||
)
|
||||
|
||||
ransomOffer.offerDetails match {
|
||||
case net.eagle0.eagle.common.diplomacy_offer.RansomOfferDetails(Some(prisonerToBeRansomed), _, _, _, _) =>
|
||||
prisonerToBeRansomed.prisonerHeroId
|
||||
case _ =>
|
||||
throw new ProtoConversionException("ResolveRansomOfferSelectedCommand ransomOffer missing prisonerToBeRansomed")
|
||||
}
|
||||
}
|
||||
|
||||
private def prisonerToBeRansomedFromProto(p: PrisonerToBeRansomedProto): PrisonerToBeRansomed =
|
||||
PrisonerToBeRansomed(
|
||||
prisonerHeroId = p.prisonerHeroId,
|
||||
|
||||
@@ -35,6 +35,7 @@ scala_library(
|
||||
"@maven//:org_postgresql_postgresql",
|
||||
],
|
||||
deps = [
|
||||
":game_controller_registry",
|
||||
":games_manager",
|
||||
":service",
|
||||
":sqlite_history",
|
||||
@@ -119,6 +120,7 @@ scala_library(
|
||||
deps = [
|
||||
":authorization_utils",
|
||||
":custom_battle_manager",
|
||||
":game_controller_registry",
|
||||
":games_manager",
|
||||
":post_results",
|
||||
":sync_response_observer",
|
||||
@@ -205,6 +207,7 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:eagle_scala_grpc",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"@maven//:io_sentry_sentry",
|
||||
],
|
||||
)
|
||||
@@ -221,6 +224,41 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "game_controller_registry",
|
||||
srcs = ["GameControllerRegistry.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/scala/net/eagle0/eagle/service/controller:game_controller",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/service/controller:game_controller",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "games_manager_state",
|
||||
srcs = ["GamesManagerState.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
|
||||
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":game_controller_registry",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:running_games_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle:user_id",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:game_parameters_utils",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "games_manager",
|
||||
srcs = ["GamesManager.scala"],
|
||||
@@ -243,7 +281,9 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":free_hero_backstory_visibility_recovery",
|
||||
":game_controller_registry",
|
||||
":game_persister_creation",
|
||||
":games_manager_state",
|
||||
":hero_name_cache",
|
||||
":llm_resolver",
|
||||
":llm_update_queuing_proxy",
|
||||
@@ -592,6 +632,7 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:jfr_events",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
|
||||
@@ -1157,7 +1198,9 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":custom_battle_manager",
|
||||
":game_controller_registry",
|
||||
":games_manager",
|
||||
":games_manager_state",
|
||||
":hero_name_cache",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_scala_grpc",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.eagle0.eagle.service
|
||||
|
||||
import io.grpc.*
|
||||
import io.sentry.Sentry
|
||||
import net.eagle0.common.SimpleTimedLogger
|
||||
|
||||
class ExceptionInterceptor extends ServerInterceptor {
|
||||
override def interceptCall[ReqT, RespT](
|
||||
@@ -10,7 +11,7 @@ class ExceptionInterceptor extends ServerInterceptor {
|
||||
next: ServerCallHandler[ReqT, RespT]
|
||||
): ServerCall.Listener[ReqT] = {
|
||||
def handleThrowable(t: Throwable): Unit = {
|
||||
println(s"Caught throwable $t")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Caught throwable $t")
|
||||
Sentry.captureException(t)
|
||||
call.close(Status.fromThrowable(t), headers)
|
||||
}
|
||||
@@ -20,7 +21,7 @@ class ExceptionInterceptor extends ServerInterceptor {
|
||||
next.startCall(call, headers)
|
||||
catch {
|
||||
case e: Throwable =>
|
||||
println(s"Caught exception $e")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Caught exception $e")
|
||||
Sentry.captureException(e)
|
||||
throw e
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import net.eagle0.eagle.model.state.game_state.GameState
|
||||
* Generated-pool heroes arrive with a pre-authored backstory and historically had no client-text visibility set at
|
||||
* placement (the all-faction request is only emitted for freshly spawned heroes with an empty backstory). Such heroes
|
||||
* could end up with their backstory visible only to a faction they happened to pass through, never to the faction
|
||||
* actually ruling the province they sit in — the [BACKSTORY-VISIBILITY] diagnostic in GameController.
|
||||
* actually ruling the province they sit in.
|
||||
*
|
||||
* Going forward this is fixed at the appearance event (UnaffiliatedHeroAppearedAction) and on movement
|
||||
* (PerformUnaffiliatedHeroesAction.heroMovedResult). This recovery repairs games that were already saved in the broken
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package net.eagle0.eagle.service
|
||||
|
||||
import net.eagle0.eagle.{GameId, UserId}
|
||||
import net.eagle0.eagle.service.controller.GameController
|
||||
|
||||
case class ControllerInfo(
|
||||
controller: GameController,
|
||||
isAiGame: Boolean,
|
||||
maxPlayers: Int,
|
||||
lastPlayedByUserId: Map[UserId, Long] = Map.empty,
|
||||
createdTimestampMillis: Long = 0L
|
||||
)
|
||||
|
||||
final case class GameControllerRegistry(
|
||||
controllers: Map[GameId, ControllerInfo]
|
||||
) {
|
||||
def snapshot: Map[GameId, ControllerInfo] = controllers
|
||||
|
||||
def contains(gameId: GameId): Boolean =
|
||||
controllers.contains(gameId)
|
||||
|
||||
def get(gameId: GameId): Option[ControllerInfo] =
|
||||
controllers.get(gameId)
|
||||
|
||||
def apply(gameId: GameId): ControllerInfo =
|
||||
controllers(gameId)
|
||||
|
||||
def size: Int =
|
||||
controllers.size
|
||||
|
||||
def keySet: Set[GameId] =
|
||||
controllers.keySet
|
||||
|
||||
def values: Iterable[ControllerInfo] =
|
||||
controllers.values
|
||||
|
||||
def map[B](f: ((GameId, ControllerInfo)) => B): Iterable[B] =
|
||||
controllers.map(f)
|
||||
|
||||
def put(gameId: GameId, controllerInfo: ControllerInfo): GameControllerRegistry =
|
||||
copy(controllers = controllers + (gameId -> controllerInfo))
|
||||
|
||||
def put(
|
||||
controller: GameController,
|
||||
isAiGame: Boolean,
|
||||
maxPlayers: Int,
|
||||
createdTimestampMillis: Long = 0L
|
||||
): GameControllerRegistry =
|
||||
put(
|
||||
controller.gameId,
|
||||
ControllerInfo(
|
||||
controller = controller,
|
||||
isAiGame = isAiGame,
|
||||
maxPlayers = maxPlayers,
|
||||
createdTimestampMillis = createdTimestampMillis
|
||||
)
|
||||
)
|
||||
|
||||
def remove(gameId: GameId): (GameControllerRegistry, Option[ControllerInfo]) =
|
||||
(copy(controllers = controllers - gameId), controllers.get(gameId))
|
||||
|
||||
def clear: (GameControllerRegistry, Iterable[ControllerInfo]) =
|
||||
(copy(controllers = Map.empty), controllers.values)
|
||||
|
||||
def withUpdatedController(gameId: GameId, controller: GameController): GameControllerRegistry =
|
||||
put(gameId, controllers(gameId).copy(controller = controller))
|
||||
|
||||
def withUpdatedController(gameId: GameId, update: GameController => GameController): GameControllerRegistry =
|
||||
withUpdatedController(gameId, update(controllers(gameId).controller))
|
||||
|
||||
def withUpdatedLastPlayed(gameId: GameId, userId: UserId, millis: Long): GameControllerRegistry =
|
||||
put(
|
||||
gameId,
|
||||
controllers(gameId).copy(
|
||||
lastPlayedByUserId = controllers(gameId).lastPlayedByUserId.updated(userId, millis)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.eagle0.eagle.service
|
||||
|
||||
import java.io.File
|
||||
|
||||
import net.eagle0.common.SimpleTimedLogger
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
@@ -57,13 +58,13 @@ private[service] object GameHistoryFactory {
|
||||
try PostgresHistory.loaded(gameId)
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"GameHistoryFactory: failed to load Postgres history for game ${gameId.toHexString}; trying SQLite fallback: ${e.getMessage}"
|
||||
)
|
||||
None
|
||||
}
|
||||
if postgres.isEmpty then
|
||||
println(
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"GameHistoryFactory: no Postgres history found for game ${gameId.toHexString}; trying SQLite fallback"
|
||||
)
|
||||
val fallback = postgres.orElse(sqlite)
|
||||
|
||||
@@ -8,7 +8,6 @@ import scala.util.{Failure, Random, Success}
|
||||
import io.sentry.Sentry
|
||||
import net.eagle0.common.{ProtoParser, SeededRandom, SimpleTimedLogger, ZipUtils}
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
|
||||
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId, GameId, ProvinceId, ShardokGameId, UserId}
|
||||
import net.eagle0.eagle.admin.game_admin.{ConvertAiToHumanResponse, ConvertHumanToAiResponse, ReassignFactionResponse}
|
||||
import net.eagle0.eagle.api.eagle.*
|
||||
@@ -75,46 +74,6 @@ case class GamePlayerInfo(
|
||||
gameType: GameTypeProto
|
||||
)
|
||||
|
||||
case class WaitingGamePlayerInfo(
|
||||
userId: UserId,
|
||||
leaderNameTextId: String
|
||||
)
|
||||
|
||||
case class GameAwaitingPlayers(
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
gameId: GameId,
|
||||
existingHumanPlayers: Vector[WaitingGamePlayerInfo],
|
||||
totalPlayerCount: Int,
|
||||
humanPlayerCount: Int,
|
||||
greatPeople: Vector[Hero]
|
||||
) {
|
||||
private def claimedNameTextIds: Vector[String] =
|
||||
existingHumanPlayers.map(_.leaderNameTextId)
|
||||
def availableNameTextIds: Vector[String] =
|
||||
greatPeople
|
||||
.map(_.nameTextId)
|
||||
.filterNot(claimedNameTextIds.contains)
|
||||
def withNewWaitingPlayerInfo(
|
||||
waitingGamePlayerInfo: WaitingGamePlayerInfo
|
||||
): GameAwaitingPlayers =
|
||||
GameAwaitingPlayers(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
gameId = gameId,
|
||||
existingHumanPlayers = existingHumanPlayers :+ waitingGamePlayerInfo,
|
||||
totalPlayerCount = totalPlayerCount,
|
||||
humanPlayerCount = humanPlayerCount,
|
||||
greatPeople = greatPeople
|
||||
)
|
||||
}
|
||||
|
||||
case class ControllerInfo(
|
||||
var controller: GameController,
|
||||
isAiGame: Boolean,
|
||||
maxPlayers: Int,
|
||||
var lastPlayedByUserId: Map[UserId, Long] = Map.empty,
|
||||
createdTimestampMillis: Long = 0L
|
||||
)
|
||||
|
||||
object GamesManager {
|
||||
private[service] val DroppedGamesKey: String = "dropped_games.e0es"
|
||||
private[service] val DroppedGameRetentionMillis: Long = TimeUnit.DAYS.toMillis(14)
|
||||
@@ -239,8 +198,8 @@ object GamesManager {
|
||||
|
||||
new GamesManager(
|
||||
shardokInternalInterface = shardokInternalInterface,
|
||||
gameControllerInfos = Map.empty, // Start empty - games load on demand
|
||||
gamesAwaitingPlayers = Vector.empty,
|
||||
initialGameControllerInfos = Map.empty, // Start empty - games load on demand
|
||||
initialGamesAwaitingPlayers = Vector.empty,
|
||||
randomGenerator = randomGenerator,
|
||||
persister = persister,
|
||||
gameCreation = gameCreation,
|
||||
@@ -453,8 +412,8 @@ object GamesManager {
|
||||
|
||||
class GamesManager(
|
||||
shardokInternalInterface: ShardokInternalInterfaceStub,
|
||||
var gameControllerInfos: Map[GameId, ControllerInfo],
|
||||
var gamesAwaitingPlayers: Vector[GameAwaitingPlayers],
|
||||
initialGameControllerInfos: Map[GameId, ControllerInfo],
|
||||
initialGamesAwaitingPlayers: Vector[GameAwaitingPlayers],
|
||||
randomGenerator: Random,
|
||||
persister: Persister,
|
||||
gameCreation: NewGameCreation,
|
||||
@@ -467,20 +426,56 @@ class GamesManager(
|
||||
) extends BattleUpdateReceiver
|
||||
with LlmUpdateReceiver {
|
||||
|
||||
// Tracks games that have been explicitly removed (archived/deleted) to prevent
|
||||
// them from being written back to games.e0es during save(). With lazy loading,
|
||||
// save() merges in-memory games with unloaded games from storage. Without this
|
||||
// tracking, a removed game would appear as "unloaded" and be preserved.
|
||||
private var removedGameIds: Set[GameId] = Set.empty
|
||||
private var lastDroppedGameCleanupCheckMillis: Long = 0L
|
||||
private var loadingGameIds: Set[GameId] = Set.empty
|
||||
|
||||
private var tutorialBattleConfigs: Map[GameId, TutorialBattleConfig] = Map.empty
|
||||
// Tracks mutable service state in one immutable value. GamesManager remains the
|
||||
// synchronization/effect boundary while state transitions move toward copy-returning helpers.
|
||||
private var state = GamesManagerState(
|
||||
controllerRegistry = GameControllerRegistry(initialGameControllerInfos),
|
||||
gamesAwaitingPlayers = initialGamesAwaitingPlayers
|
||||
)
|
||||
|
||||
// Per-game locks to prevent concurrent loading of the same game and to allow
|
||||
// game loading to happen without holding the main GamesManager.this lock.
|
||||
private val gameLoadingLocks = new ConcurrentHashMap[GameId, AnyRef]()
|
||||
|
||||
def gameControllerInfos: Map[GameId, ControllerInfo] = this.synchronized {
|
||||
state.controllerRegistry.snapshot
|
||||
}
|
||||
|
||||
def gamesAwaitingPlayers: Vector[GameAwaitingPlayers] = this.synchronized {
|
||||
state.gamesAwaitingPlayers
|
||||
}
|
||||
|
||||
private def loadingGameIds: Set[GameId] =
|
||||
state.loadingGameIds
|
||||
|
||||
private def updateState(update: GamesManagerState => GamesManagerState): Unit = this.synchronized {
|
||||
state = update(state)
|
||||
}
|
||||
|
||||
private def putControllerInfo(gameId: GameId, controllerInfo: ControllerInfo): Unit =
|
||||
updateState(_.withControllerInfo(gameId, controllerInfo))
|
||||
|
||||
private def updateController(gameId: GameId, controller: GameController): Unit =
|
||||
updateState(state =>
|
||||
state.withControllerRegistry(
|
||||
state.controllerRegistry.withUpdatedController(gameId, controller)
|
||||
)
|
||||
)
|
||||
|
||||
private def updateController(gameId: GameId, update: GameController => GameController): Unit =
|
||||
updateState(state =>
|
||||
state.withControllerRegistry(
|
||||
state.controllerRegistry.withUpdatedController(gameId, update)
|
||||
)
|
||||
)
|
||||
|
||||
private def updateLastPlayed(gameId: GameId, userId: UserId): Unit =
|
||||
updateState(state =>
|
||||
state.withControllerRegistry(
|
||||
state.controllerRegistry.withUpdatedLastPlayed(gameId, userId, System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
|
||||
val shardokClient =
|
||||
new ShardokInterfaceGrpcClient(shardokInternalInterface, this)
|
||||
|
||||
@@ -501,17 +496,16 @@ class GamesManager(
|
||||
try info.controller.clientTextStore.close()
|
||||
finally GamesManager.extractFullHistory(info.controller.engine).close()
|
||||
|
||||
private def unloadGame(gameId: GameId): Option[ControllerInfo] = {
|
||||
val existing = gameControllerInfos.get(gameId)
|
||||
gameControllerInfos -= gameId
|
||||
private def unloadGame(gameId: GameId): Option[ControllerInfo] =
|
||||
val (updatedRegistry, existing) = state.controllerRegistry.remove(gameId)
|
||||
updateState(_.withControllerRegistry(updatedRegistry))
|
||||
existing.foreach(closeControllerInfo)
|
||||
existing
|
||||
}
|
||||
|
||||
private def unloadAllGames(): Unit = {
|
||||
gameControllerInfos.values.foreach(closeControllerInfo)
|
||||
gameControllerInfos = Map.empty
|
||||
}
|
||||
private def unloadAllGames(): Unit =
|
||||
val (updatedRegistry, existing) = state.controllerRegistry.clear
|
||||
updateState(_.withControllerRegistry(updatedRegistry))
|
||||
existing.foreach(closeControllerInfo)
|
||||
|
||||
def recreateLlmCallers(): Unit =
|
||||
llmResolver.recreateLlmCallers()
|
||||
@@ -523,18 +517,6 @@ class GamesManager(
|
||||
shardokClient.connect()
|
||||
}
|
||||
|
||||
// Track the flush marker mod time to detect when it's been updated
|
||||
@volatile private var lastFlushMarkerCheck: Long = 0L
|
||||
@volatile private var flushMarkerModTime: Long = 0L
|
||||
|
||||
/**
|
||||
* Cached snapshot of games.e0es. Read paths share this so we don't re-parse the file on every lobby update,
|
||||
* heartbeat-driven refresh, save() merge, or game-load. Invalidated on flush-marker bump (covers blue-green
|
||||
* deployments where the active server has flushed newer state) and refreshed at the end of save() (which is the only
|
||||
* path through this process that can change the file's contents).
|
||||
*/
|
||||
@volatile private var cachedRunningGames: Option[Vector[RunningGame]] = None
|
||||
|
||||
/**
|
||||
* Checks if the flush marker has been updated since we last loaded games. If so, invalidates the cache so games
|
||||
* reload from fresh disk state.
|
||||
@@ -545,25 +527,23 @@ class GamesManager(
|
||||
private def invalidateCacheIfFlushMarkerUpdated(): Unit = {
|
||||
val now = System.currentTimeMillis()
|
||||
// Only check every 100ms to avoid excessive file system access
|
||||
if now - lastFlushMarkerCheck >= 100 then {
|
||||
lastFlushMarkerCheck = now
|
||||
if now - state.lastFlushMarkerCheck >= 100 then {
|
||||
updateState(_.withLastFlushMarkerCheck(now))
|
||||
|
||||
val flushMarker = java.nio.file.Paths.get(SaveDirectory.saveDirectory, ".flush_complete")
|
||||
if java.nio.file.Files.exists(flushMarker) then
|
||||
try {
|
||||
val currentModTime = java.nio.file.Files.getLastModifiedTime(flushMarker).toMillis
|
||||
if currentModTime > flushMarkerModTime then {
|
||||
if currentModTime > state.flushMarkerModTime then {
|
||||
val cachedCount = gameControllerInfos.size
|
||||
if cachedCount > 0 then {
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[DEPLOY] Flush marker updated, invalidating $cachedCount cached games"
|
||||
)
|
||||
unloadAllGames()
|
||||
gamesAwaitingPlayers = Vector.empty
|
||||
removedGameIds = Set.empty
|
||||
cachedRunningGames = None
|
||||
updateState(_.withoutLoadedGames)
|
||||
}
|
||||
flushMarkerModTime = currentModTime
|
||||
updateState(_.withFlushMarkerModTime(currentModTime))
|
||||
}
|
||||
} catch {
|
||||
case _: Exception => // Ignore errors checking marker
|
||||
@@ -580,9 +560,9 @@ class GamesManager(
|
||||
*/
|
||||
private def runningGamesFromStorage(): Vector[RunningGame] = {
|
||||
invalidateCacheIfFlushMarkerUpdated()
|
||||
cachedRunningGames.getOrElse {
|
||||
state.cachedRunningGames.getOrElse {
|
||||
val games = readRunningGamesFromStorage()
|
||||
cachedRunningGames = Some(games)
|
||||
updateState(_.withCachedRunningGames(games))
|
||||
games
|
||||
}
|
||||
}
|
||||
@@ -655,8 +635,8 @@ class GamesManager(
|
||||
}
|
||||
|
||||
private def cleanupExpiredDroppedGames(nowMillis: Long): Unit =
|
||||
if nowMillis - lastDroppedGameCleanupCheckMillis >= GamesManager.DroppedGameCleanupIntervalMillis then {
|
||||
lastDroppedGameCleanupCheckMillis = nowMillis
|
||||
if nowMillis - state.lastDroppedGameCleanupCheckMillis >= GamesManager.DroppedGameCleanupIntervalMillis then {
|
||||
updateState(_.withLastDroppedGameCleanupCheck(nowMillis))
|
||||
val droppedGames = readDroppedGamesFromStorage()
|
||||
val (expired, retained) =
|
||||
droppedGames.partition(droppedGame =>
|
||||
@@ -830,7 +810,7 @@ class GamesManager(
|
||||
}
|
||||
|
||||
this.synchronized {
|
||||
loadingGameIds += gameId
|
||||
updateState(_.withLoadingGame(gameId))
|
||||
}
|
||||
|
||||
// Wait for flush marker before loading from disk to ensure we get fresh data
|
||||
@@ -881,12 +861,15 @@ class GamesManager(
|
||||
// Add to in-memory map BEFORE text handling so isGameValid returns true
|
||||
// during LLM request processing. Brief lock acquisition only.
|
||||
this.synchronized {
|
||||
gameControllerInfos += gameId -> ControllerInfo(
|
||||
controller = loadedControllerWithResults.gameController,
|
||||
isAiGame = false,
|
||||
maxPlayers = 2,
|
||||
lastPlayedByUserId = lastPlayedByUserId,
|
||||
createdTimestampMillis = runningGame.createdTimestampMillis
|
||||
putControllerInfo(
|
||||
gameId,
|
||||
ControllerInfo(
|
||||
controller = loadedControllerWithResults.gameController,
|
||||
isAiGame = false,
|
||||
maxPlayers = 2,
|
||||
lastPlayedByUserId = lastPlayedByUserId,
|
||||
createdTimestampMillis = runningGame.createdTimestampMillis
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -912,12 +895,15 @@ class GamesManager(
|
||||
|
||||
// Update with initialized controller. Brief lock acquisition only.
|
||||
this.synchronized {
|
||||
gameControllerInfos += gameId -> ControllerInfo(
|
||||
controller = withLiveHumanClientsFromCurrentController(initializedController),
|
||||
isAiGame = false,
|
||||
maxPlayers = 2,
|
||||
lastPlayedByUserId = lastPlayedByUserId,
|
||||
createdTimestampMillis = runningGame.createdTimestampMillis
|
||||
putControllerInfo(
|
||||
gameId,
|
||||
ControllerInfo(
|
||||
controller = withLiveHumanClientsFromCurrentController(initializedController),
|
||||
isAiGame = false,
|
||||
maxPlayers = 2,
|
||||
lastPlayedByUserId = lastPlayedByUserId,
|
||||
createdTimestampMillis = runningGame.createdTimestampMillis
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -938,7 +924,7 @@ class GamesManager(
|
||||
}
|
||||
finally {
|
||||
this.synchronized {
|
||||
loadingGameIds -= gameId
|
||||
updateState(_.withoutLoadingGame(gameId))
|
||||
}
|
||||
gameLoadingLocks.remove(gameId, lock): Unit
|
||||
}
|
||||
@@ -1328,7 +1314,7 @@ class GamesManager(
|
||||
// Start with existing games that haven't been loaded (preserve them),
|
||||
// but exclude games that were explicitly removed (archived/deleted)
|
||||
val unloadedGames =
|
||||
existingGames.filterNot(rg => loadedGameIds.contains(rg.gameId) || removedGameIds.contains(rg.gameId))
|
||||
existingGames.filterNot(rg => loadedGameIds.contains(rg.gameId) || state.removedGameIds.contains(rg.gameId))
|
||||
|
||||
// Add in-memory games (these have the freshest state)
|
||||
val loadedGames = gameControllerInfos.map {
|
||||
@@ -1374,10 +1360,10 @@ class GamesManager(
|
||||
|
||||
// Refresh the cache from the just-written state. The on-disk file and the cache now agree, so subsequent
|
||||
// read paths can hit the cache instead of re-parsing.
|
||||
cachedRunningGames = Some(rg.runningGames.toVector)
|
||||
updateState(_.withCachedRunningGames(rg.runningGames.toVector))
|
||||
|
||||
// Clear removed games after successful save
|
||||
removedGameIds = Set.empty
|
||||
updateState(_.withoutRemovedGames)
|
||||
()
|
||||
}
|
||||
|
||||
@@ -1387,11 +1373,15 @@ class GamesManager(
|
||||
maxPlayers: Int,
|
||||
createdTimestampMillis: Long = 0L
|
||||
): GameController = this.synchronized {
|
||||
gameControllerInfos += controller.gameId -> ControllerInfo(
|
||||
controller = controller,
|
||||
isAiGame = isAiGame,
|
||||
maxPlayers = maxPlayers,
|
||||
createdTimestampMillis = createdTimestampMillis
|
||||
updateState(state =>
|
||||
state.withControllerRegistry(
|
||||
state.controllerRegistry.put(
|
||||
controller = controller,
|
||||
isAiGame = isAiGame,
|
||||
maxPlayers = maxPlayers,
|
||||
createdTimestampMillis = createdTimestampMillis
|
||||
)
|
||||
)
|
||||
)
|
||||
controller
|
||||
}
|
||||
@@ -1441,7 +1431,7 @@ class GamesManager(
|
||||
battle = ShardokBattleConverter.toProto(battle),
|
||||
eagleGameState = gameState,
|
||||
humanFactions = humanFactions,
|
||||
tutorialBattleConfig = tutorialBattleConfigs.get(battle.eagleGameId)
|
||||
tutorialBattleConfig = state.tutorialBattleConfigs.get(battle.eagleGameId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1491,8 +1481,7 @@ class GamesManager(
|
||||
)
|
||||
): Unit
|
||||
// Record last played time for this user
|
||||
val ci = gameControllerInfos(gameId)
|
||||
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
|
||||
updateLastPlayed(gameId, userId)
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -1519,8 +1508,7 @@ class GamesManager(
|
||||
)
|
||||
): Unit
|
||||
// Record last played time for this user
|
||||
val ci = gameControllerInfos(gameId)
|
||||
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
|
||||
updateLastPlayed(gameId, userId)
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -1580,8 +1568,7 @@ class GamesManager(
|
||||
)
|
||||
}
|
||||
// Record last played time for this user
|
||||
val ci = gameControllerInfos(eagleGameId)
|
||||
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
|
||||
updateLastPlayed(eagleGameId, userId)
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -1653,7 +1640,7 @@ class GamesManager(
|
||||
withLiveHumanClientsFromCurrentController(afterUnrequested)
|
||||
}
|
||||
|
||||
gameControllerInfos(cwrt.gameController.gameId).controller = newController
|
||||
updateController(cwrt.gameController.gameId, newController)
|
||||
|
||||
GameControllerWithPostResults(
|
||||
gameController = newController,
|
||||
@@ -1686,8 +1673,9 @@ class GamesManager(
|
||||
)
|
||||
|
||||
val filterStart = System.currentTimeMillis()
|
||||
gameControllerInfos(gameId).controller = gameControllerInfos(gameId).controller
|
||||
.streamUpdates(
|
||||
updateController(
|
||||
gameId,
|
||||
_.streamUpdates(
|
||||
userId = userId,
|
||||
clientId = clientId,
|
||||
knownResultCount = knownResultCount,
|
||||
@@ -1695,6 +1683,7 @@ class GamesManager(
|
||||
responseObserver = responseObserver,
|
||||
streamingTextStatuses = streamingTextStatuses
|
||||
)
|
||||
)
|
||||
val filterEnd = System.currentTimeMillis()
|
||||
if filterEnd - filterStart > 500 then
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
@@ -1713,8 +1702,7 @@ class GamesManager(
|
||||
gameId: GameId
|
||||
): Unit =
|
||||
this.synchronized {
|
||||
gameControllerInfos(gameId).controller = gameControllerInfos(gameId).controller
|
||||
.stopStreaming(userId = userId)
|
||||
updateController(gameId, _.stopStreaming(userId = userId))
|
||||
}
|
||||
|
||||
def joinGame(
|
||||
@@ -1733,16 +1721,14 @@ class GamesManager(
|
||||
)
|
||||
|
||||
if newGap.existingHumanPlayers.size == newGap.humanPlayerCount then {
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers.diff(Vector(gap))
|
||||
updateState(_.withGamesAwaitingPlayers(gamesAwaitingPlayers.diff(Vector(gap))))
|
||||
synchronizedCreateGame(
|
||||
expandedGameParameters = gap.expandedGameParameters,
|
||||
gameId = newGap.gameId,
|
||||
humanPlayers = newGap.existingHumanPlayers,
|
||||
aiPlayerCount = newGap.totalPlayerCount - newGap.existingHumanPlayers.size
|
||||
): Unit
|
||||
} else
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers
|
||||
.diff(Vector(gap)) :+ newGap
|
||||
} else updateState(_.withGamesAwaitingPlayers(gamesAwaitingPlayers.diff(Vector(gap)) :+ newGap))
|
||||
|
||||
JoinGameResponse(
|
||||
result = JoinGameResult.SUCCESS_JOIN_GAME_RESULT,
|
||||
@@ -1778,19 +1764,23 @@ class GamesManager(
|
||||
desiredLeaderTextId: String,
|
||||
maxHumanPlayers: Int,
|
||||
totalPlayers: Int
|
||||
): CreateGameResponse =
|
||||
): CreateGameResponse = this.synchronized {
|
||||
if maxHumanPlayers > totalPlayers || totalPlayers > MaxSupportedPlayers.intValue
|
||||
then CreateGameResponse(INVALID_OPTIONS_RESULT, 0)
|
||||
else {
|
||||
val newGameId = randomGenerator.nextLong()
|
||||
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers :+ GameAwaitingPlayers(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
gameId = newGameId,
|
||||
existingHumanPlayers = Vector.empty,
|
||||
totalPlayerCount = totalPlayers,
|
||||
humanPlayerCount = maxHumanPlayers,
|
||||
greatPeople = expandedGameParameters.greatPeopleHeroProtos
|
||||
updateState(
|
||||
_.withGamesAwaitingPlayers(
|
||||
gamesAwaitingPlayers :+ GameAwaitingPlayers(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
gameId = newGameId,
|
||||
existingHumanPlayers = Vector.empty,
|
||||
totalPlayerCount = totalPlayers,
|
||||
humanPlayerCount = maxHumanPlayers,
|
||||
greatPeople = expandedGameParameters.greatPeopleHeroProtos
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = joinGame(
|
||||
@@ -1805,12 +1795,13 @@ class GamesManager(
|
||||
save()
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
def createTutorialGameForUser(
|
||||
userId: UserId,
|
||||
gameId: GameId,
|
||||
startPhase: TutorialPhase = TutorialPhase.OpeningBattle
|
||||
): CreateGameResponse = {
|
||||
): CreateGameResponse = this.synchronized {
|
||||
val (engineAndResults, tutorialConfig) =
|
||||
TutorialGameCreation.createTutorialGame(
|
||||
gameId,
|
||||
@@ -1818,8 +1809,7 @@ class GamesManager(
|
||||
GamesManager.battalionTypes
|
||||
)
|
||||
|
||||
if startPhase == TutorialPhase.OpeningBattle then
|
||||
tutorialBattleConfigs = tutorialBattleConfigs + (gameId -> tutorialConfig)
|
||||
if startPhase == TutorialPhase.OpeningBattle then updateState(_.withTutorialBattleConfig(gameId, tutorialConfig))
|
||||
|
||||
val initialTextStore = GamesManager.createClientTextStoreWithData(
|
||||
gameId = gameId,
|
||||
@@ -1909,7 +1899,7 @@ class GamesManager(
|
||||
else {
|
||||
shardokClient.cancelBattlesForGame(gameId)
|
||||
val (newController, resyncedCount) = controller.rewindTo(targetActionCount)
|
||||
gameControllerInfos(gameId).controller = newController
|
||||
updateController(gameId, newController)
|
||||
val afterContinuation = synchronizedHandlePostResults(
|
||||
GameController.performAutomaticContinuation(
|
||||
GameControllerWithPostResults(
|
||||
@@ -1931,7 +1921,7 @@ class GamesManager(
|
||||
gameId: GameId,
|
||||
humanPlayers: Vector[WaitingGamePlayerInfo],
|
||||
aiPlayerCount: Int
|
||||
): GameControllerWithPostResults = {
|
||||
): GameControllerWithPostResults = this.synchronized {
|
||||
val logger = SimpleTimedLogger.printLogger
|
||||
val gameIdHex = gameId.toHexString
|
||||
logger.logLine(s"[CREATE:$gameIdHex] Starting game creation")
|
||||
@@ -2051,13 +2041,15 @@ class GamesManager(
|
||||
val remainingPlayers = gap.existingHumanPlayers.filterNot(_.userId == userId)
|
||||
if remainingPlayers.isEmpty then
|
||||
// Remove the waiting game entirely
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers.filterNot(_.gameId == gameId)
|
||||
updateState(_.withGamesAwaitingPlayers(gamesAwaitingPlayers.filterNot(_.gameId == gameId)))
|
||||
else
|
||||
// Keep the game but remove this user
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers.map { g =>
|
||||
if g.gameId == gameId then g.copy(existingHumanPlayers = remainingPlayers)
|
||||
else g
|
||||
}
|
||||
updateState(
|
||||
_.withGamesAwaitingPlayers(gamesAwaitingPlayers.map { g =>
|
||||
if g.gameId == gameId then g.copy(existingHumanPlayers = remainingPlayers)
|
||||
else g
|
||||
})
|
||||
)
|
||||
save()
|
||||
DropGameResponse(
|
||||
result = DropGameResult.SUCCESS_DROP_GAME_RESULT,
|
||||
@@ -2090,7 +2082,7 @@ class GamesManager(
|
||||
// the lobby update reflects the removal right away. Keep the underlying game data in
|
||||
// its current backend so old games remain inspectable without storage-format conversion.
|
||||
unloadGame(gameId): Unit
|
||||
removedGameIds += gameId // Track removal to prevent re-adding from storage
|
||||
updateState(_.withRemovedGame(gameId)) // Track removal to prevent re-adding from storage
|
||||
save()
|
||||
recordDroppedGame(gameId, System.currentTimeMillis())
|
||||
Future {
|
||||
@@ -2107,7 +2099,7 @@ class GamesManager(
|
||||
} else {
|
||||
// Other humans remain - convert to AI
|
||||
val updatedController = controller.dropUser(userId)
|
||||
gameControllerInfos(gameId).controller = updatedController
|
||||
updateController(gameId, updatedController)
|
||||
|
||||
// Trigger AI commands if it's now their turn
|
||||
val currentState = updatedController.engine.currentState
|
||||
@@ -2121,7 +2113,7 @@ class GamesManager(
|
||||
)
|
||||
)
|
||||
)
|
||||
gameControllerInfos(gameId).controller = gcwpr.gameController
|
||||
updateController(gameId, gcwpr.gameController)
|
||||
save()
|
||||
DropGameResponse(
|
||||
result = DropGameResult.SUCCESS_DROP_GAME_RESULT,
|
||||
@@ -2180,7 +2172,7 @@ class GamesManager(
|
||||
)
|
||||
else {
|
||||
val updatedController = controller.promoteAiToHuman(factionId, newUserId)
|
||||
gameControllerInfos(gameId).controller = updatedController
|
||||
updateController(gameId, updatedController)
|
||||
save()
|
||||
ConvertAiToHumanResponse(result = Result.SUCCESS)
|
||||
}
|
||||
@@ -2232,7 +2224,7 @@ class GamesManager(
|
||||
)
|
||||
)
|
||||
)
|
||||
gameControllerInfos(gameId).controller = gcwpr.gameController
|
||||
updateController(gameId, gcwpr.gameController)
|
||||
save()
|
||||
ConvertHumanToAiResponse(result = Result.SUCCESS)
|
||||
}
|
||||
@@ -2283,7 +2275,7 @@ class GamesManager(
|
||||
)
|
||||
|
||||
case Some((previousUserId, updatedController)) =>
|
||||
gameControllerInfos(gameId).controller = updatedController
|
||||
updateController(gameId, updatedController)
|
||||
save()
|
||||
ReassignFactionResponse(
|
||||
result = Result.SUCCESS,
|
||||
@@ -2364,7 +2356,7 @@ class GamesManager(
|
||||
// Track removal and save if the game was running or had save files deleted.
|
||||
// This ensures the game is removed from games.e0es even if it wasn't loaded.
|
||||
if wasRunning || deleteSaveFiles then {
|
||||
removedGameIds += gameId
|
||||
updateState(_.withRemovedGame(gameId))
|
||||
save()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.eagle0.eagle.service
|
||||
|
||||
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
|
||||
import net.eagle0.eagle.{GameId, UserId}
|
||||
import net.eagle0.eagle.internal.hero.Hero
|
||||
import net.eagle0.eagle.internal.running_games.RunningGame
|
||||
import net.eagle0.eagle.service.new_game_creation.ExpandedGameParameters
|
||||
|
||||
case class WaitingGamePlayerInfo(
|
||||
userId: UserId,
|
||||
leaderNameTextId: String
|
||||
)
|
||||
|
||||
case class GameAwaitingPlayers(
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
gameId: GameId,
|
||||
existingHumanPlayers: Vector[WaitingGamePlayerInfo],
|
||||
totalPlayerCount: Int,
|
||||
humanPlayerCount: Int,
|
||||
greatPeople: Vector[Hero]
|
||||
) {
|
||||
private def claimedNameTextIds: Vector[String] =
|
||||
existingHumanPlayers.map(_.leaderNameTextId)
|
||||
|
||||
def availableNameTextIds: Vector[String] =
|
||||
greatPeople
|
||||
.map(_.nameTextId)
|
||||
.filterNot(claimedNameTextIds.contains)
|
||||
|
||||
def withNewWaitingPlayerInfo(
|
||||
waitingGamePlayerInfo: WaitingGamePlayerInfo
|
||||
): GameAwaitingPlayers =
|
||||
copy(existingHumanPlayers = existingHumanPlayers :+ waitingGamePlayerInfo)
|
||||
}
|
||||
|
||||
case class GamesManagerState(
|
||||
controllerRegistry: GameControllerRegistry,
|
||||
gamesAwaitingPlayers: Vector[GameAwaitingPlayers],
|
||||
removedGameIds: Set[GameId] = Set.empty,
|
||||
lastDroppedGameCleanupCheckMillis: Long = 0L,
|
||||
loadingGameIds: Set[GameId] = Set.empty,
|
||||
tutorialBattleConfigs: Map[GameId, TutorialBattleConfig] = Map.empty,
|
||||
lastFlushMarkerCheck: Long = 0L,
|
||||
flushMarkerModTime: Long = 0L,
|
||||
cachedRunningGames: Option[Vector[RunningGame]] = None
|
||||
) {
|
||||
def withControllerInfo(gameId: GameId, controllerInfo: ControllerInfo): GamesManagerState =
|
||||
copy(controllerRegistry = controllerRegistry.put(gameId, controllerInfo))
|
||||
|
||||
def withControllerRegistry(updatedControllerRegistry: GameControllerRegistry): GamesManagerState =
|
||||
copy(controllerRegistry = updatedControllerRegistry)
|
||||
|
||||
def withLoadingGame(gameId: GameId): GamesManagerState =
|
||||
copy(loadingGameIds = loadingGameIds + gameId)
|
||||
|
||||
def withoutLoadingGame(gameId: GameId): GamesManagerState =
|
||||
copy(loadingGameIds = loadingGameIds - gameId)
|
||||
|
||||
def withGamesAwaitingPlayers(updatedGamesAwaitingPlayers: Vector[GameAwaitingPlayers]): GamesManagerState =
|
||||
copy(gamesAwaitingPlayers = updatedGamesAwaitingPlayers)
|
||||
|
||||
def withTutorialBattleConfig(gameId: GameId, tutorialConfig: TutorialBattleConfig): GamesManagerState =
|
||||
copy(tutorialBattleConfigs = tutorialBattleConfigs + (gameId -> tutorialConfig))
|
||||
|
||||
def withLastDroppedGameCleanupCheck(millis: Long): GamesManagerState =
|
||||
copy(lastDroppedGameCleanupCheckMillis = millis)
|
||||
|
||||
def withLastFlushMarkerCheck(millis: Long): GamesManagerState =
|
||||
copy(lastFlushMarkerCheck = millis)
|
||||
|
||||
def withFlushMarkerModTime(millis: Long): GamesManagerState =
|
||||
copy(flushMarkerModTime = millis)
|
||||
|
||||
def withRemovedGame(gameId: GameId): GamesManagerState =
|
||||
copy(removedGameIds = removedGameIds + gameId)
|
||||
|
||||
def withoutRemovedGames: GamesManagerState =
|
||||
copy(removedGameIds = Set.empty)
|
||||
|
||||
def withoutLoadedGames: GamesManagerState =
|
||||
copy(
|
||||
controllerRegistry = GameControllerRegistry(Map.empty),
|
||||
gamesAwaitingPlayers = Vector.empty,
|
||||
removedGameIds = Set.empty,
|
||||
cachedRunningGames = None
|
||||
)
|
||||
|
||||
def withCachedRunningGames(runningGames: Vector[RunningGame]): GamesManagerState =
|
||||
copy(cachedRunningGames = Some(runningGames))
|
||||
|
||||
def withoutCachedRunningGames: GamesManagerState =
|
||||
copy(cachedRunningGames = None)
|
||||
}
|
||||
@@ -1041,8 +1041,6 @@ final case class GameController(
|
||||
case gc => gc
|
||||
}
|
||||
|
||||
debugCheckFreeHeroBackstoryVisibility(newGc)
|
||||
|
||||
GameControllerWithPostResults(
|
||||
gameController = newGc,
|
||||
postResults = PostResults(
|
||||
@@ -1053,42 +1051,6 @@ final case class GameController(
|
||||
)
|
||||
}
|
||||
|
||||
// TEMPORARY: Log warnings when a free hero's backstory is not visible to the province's ruling faction.
|
||||
// This helps find cases where visibility isn't being extended properly.
|
||||
private def debugCheckFreeHeroBackstoryVisibility(gc: GameController): Unit = {
|
||||
val gs = gc.engine.currentState
|
||||
val cts = gc.clientTextStore
|
||||
val freeHeroBackstories = gs.provinces.values.toVector.flatMap { province =>
|
||||
province.rulingFactionId.toVector.flatMap { rulingFactionId =>
|
||||
province.unaffiliatedHeroes.flatMap { uh =>
|
||||
gs.heroes.get(uh.heroId).toVector.flatMap { hero =>
|
||||
hero.backstoryVersions.lastOption.toVector.map { latestVersion =>
|
||||
(province, rulingFactionId, uh, latestVersion.textId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if freeHeroBackstories.nonEmpty then {
|
||||
val visibilityByTextId = cts.accessibleToForIds(
|
||||
freeHeroBackstories.map(_._4).toSet
|
||||
)
|
||||
for {
|
||||
(province, rulingFactionId, uh, textId) <- freeHeroBackstories
|
||||
} {
|
||||
val accessibleTo = visibilityByTextId.getOrElse(textId, Vector.empty)
|
||||
if accessibleTo.nonEmpty && !accessibleTo.contains(rulingFactionId) then
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[BACKSTORY-VISIBILITY] game=${gs.gameId} historyCount=${gc.engine.history.count}" +
|
||||
s" round=${gs.currentRoundId} phase=${gs.currentPhase}" +
|
||||
s" | Hero ${uh.heroId} in province ${province.id} (ruled by fid $rulingFactionId)" +
|
||||
s" has backstory '$textId' visible only to fids [${accessibleTo.mkString(", ")}]" +
|
||||
s" — ruling faction CANNOT see it. UH type: ${uh.unaffiliatedHeroType}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def streamUpdates(
|
||||
userId: UserId,
|
||||
clientId: String,
|
||||
|
||||
@@ -173,6 +173,7 @@ scala_library(
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:grpc_retrier",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_to_constitution_xp_multiplier",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
|
||||
+18
-15
@@ -7,6 +7,7 @@ import scala.util.{Failure, Success}
|
||||
import io.grpc.{Status as GrpcStatus, StatusRuntimeException}
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.sentry.Sentry
|
||||
import net.eagle0.common.{GrpcRetrier, SimpleTimedLogger}
|
||||
import net.eagle0.common.common_unit.*
|
||||
import net.eagle0.common.game_setup_info.{GameSetupInfo, NewGameRequest}
|
||||
import net.eagle0.common.player_info.{AlliedPlayer, PlayerSetupInfo}
|
||||
@@ -15,7 +16,6 @@ import net.eagle0.common.shardok_internal_interface.*
|
||||
import net.eagle0.common.shardok_internal_interface.GameStatusResponse.Specific
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
|
||||
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
|
||||
import net.eagle0.common.GrpcRetrier
|
||||
import net.eagle0.eagle.{FactionId, GameId, ShardokGameId}
|
||||
import net.eagle0.eagle.internal.army.MovingArmy
|
||||
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
|
||||
@@ -118,13 +118,12 @@ class ShardokInterfaceGrpcClient(
|
||||
}
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(s"Exception processing stream response for $shardokGameId: $e")
|
||||
e.printStackTrace()
|
||||
SimpleTimedLogger.printLogger.logLine(s"Exception processing stream response for $shardokGameId: $e")
|
||||
Sentry.captureException(e): Unit
|
||||
}
|
||||
|
||||
override def onError(t: Throwable): Unit = {
|
||||
println(s"Stream error for $shardokGameId: $t")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Stream error for $shardokGameId: $t")
|
||||
t match {
|
||||
case sre: StatusRuntimeException
|
||||
if sre.getStatus.getCode == GrpcStatus.Code.UNAVAILABLE ||
|
||||
@@ -137,7 +136,7 @@ class ShardokInterfaceGrpcClient(
|
||||
}
|
||||
|
||||
override def onCompleted(): Unit =
|
||||
println(s"Stream completed for $shardokGameId")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Stream completed for $shardokGameId")
|
||||
}
|
||||
|
||||
ShardokLatencyTrace.trace(
|
||||
@@ -164,11 +163,11 @@ class ShardokInterfaceGrpcClient(
|
||||
pendingBattles.synchronized {
|
||||
pendingBattles.get(shardokGameId) match {
|
||||
case Some((_, storedRequest)) =>
|
||||
println(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
|
||||
try subscribeToGame(eagleGameId, shardokGameId, Some(storedRequest))
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(s"Reconnect failed for $shardokGameId: $e")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Reconnect failed for $shardokGameId: $e")
|
||||
Sentry.captureException(e): Unit
|
||||
// Exponential backoff with max 10 seconds
|
||||
val nextDelay = Math.min(delayMs * 2, 10000)
|
||||
@@ -284,7 +283,7 @@ class ShardokInterfaceGrpcClient(
|
||||
// Empty response - nothing to process
|
||||
}
|
||||
case None =>
|
||||
println(s"No pending battle found for ${update.gameId}")
|
||||
SimpleTimedLogger.printLogger.logLine(s"No pending battle found for ${update.gameId}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +308,7 @@ class ShardokInterfaceGrpcClient(
|
||||
humanFactions: Set[FactionId],
|
||||
tutorialBattleConfig: Option[TutorialBattleConfig] = None
|
||||
): Unit = {
|
||||
println(
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"${battle.shardokGameId} sending armies " +
|
||||
s"${battle.players.map(sp => sp.getArmyGroup.armies.flatMap(_.getArmy.units).map(_.heroId).sorted.mkString(" ")).mkString(", ")}"
|
||||
)
|
||||
@@ -412,11 +411,11 @@ class ShardokInterfaceGrpcClient(
|
||||
"error" -> e.getClass.getName
|
||||
)
|
||||
)
|
||||
println(s"Failed to post command: $e")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Failed to post command: $e")
|
||||
Sentry.captureException(e): Unit
|
||||
throw e
|
||||
}
|
||||
else print("Invalid token\n")
|
||||
else SimpleTimedLogger.printLogger.logLine("Invalid token")
|
||||
|
||||
override def postPlacementCommands(
|
||||
eagleGameId: GameId,
|
||||
@@ -486,11 +485,11 @@ class ShardokInterfaceGrpcClient(
|
||||
"error" -> e.getClass.getName
|
||||
)
|
||||
)
|
||||
println(s"Failed to post placement command: $e")
|
||||
SimpleTimedLogger.printLogger.logLine(s"Failed to post placement command: $e")
|
||||
Sentry.captureException(e): Unit
|
||||
throw e
|
||||
}
|
||||
else print("Invalid token\n")
|
||||
else SimpleTimedLogger.printLogger.logLine("Invalid token")
|
||||
|
||||
override def getHexMap(mapName: ShardokGameId): Future[HexMap] =
|
||||
retrier
|
||||
@@ -511,11 +510,15 @@ class ShardokInterfaceGrpcClient(
|
||||
|
||||
toRemove.foreach { shardokGameId =>
|
||||
val _ = pendingBattles.remove(shardokGameId)
|
||||
println(s"Cancelled pending Shardok battle $shardokGameId for deleted Eagle game $eagleGameId")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Cancelled pending Shardok battle $shardokGameId for deleted Eagle game $eagleGameId"
|
||||
)
|
||||
}
|
||||
|
||||
if toRemove.nonEmpty then
|
||||
println(s"Cancelled ${toRemove.size} Shardok battle(s) for deleted Eagle game $eagleGameId")
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"Cancelled ${toRemove.size} Shardok battle(s) for deleted Eagle game $eagleGameId"
|
||||
)
|
||||
}
|
||||
|
||||
private def isArcheryCapable(eu: EagleUnit, gs: GameState): Boolean =
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_test")
|
||||
load("//tools:copts.bzl", "TEST_COPTS")
|
||||
|
||||
cc_test(
|
||||
name = "byte_vector_test",
|
||||
srcs = ["ByteVector_test.cpp"],
|
||||
copts = TEST_COPTS,
|
||||
linkstatic = True,
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:byte_vector",
|
||||
"@googletest//:gtest",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "iterable_bitset_test",
|
||||
srcs = ["IterableBitset_test.cpp"],
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
|
||||
|
||||
TEST(ByteVectorTest, fromHex_parsesMixedCasePairs) {
|
||||
const byte_vector bytes = byte_vector::FromHex("00aF10");
|
||||
|
||||
EXPECT_EQ(bytes, std::vector<uint8_t>({0x00, 0xAF, 0x10}));
|
||||
}
|
||||
|
||||
TEST(ByteVectorTest, fromHex_acceptsOptionalHexPrefixes) {
|
||||
EXPECT_EQ(byte_vector::FromHex("0x0123"), std::vector<uint8_t>({0x01, 0x23}));
|
||||
EXPECT_EQ(byte_vector::FromHex("0X0123"), std::vector<uint8_t>({0x01, 0x23}));
|
||||
EXPECT_EQ(byte_vector::FromHex("x0123"), std::vector<uint8_t>({0x01, 0x23}));
|
||||
EXPECT_EQ(byte_vector::FromHex("X0123"), std::vector<uint8_t>({0x01, 0x23}));
|
||||
}
|
||||
|
||||
TEST(ByteVectorTest, fromHex_emptyStringReturnsEmptyVector) {
|
||||
EXPECT_TRUE(byte_vector::FromHex("").empty());
|
||||
}
|
||||
|
||||
TEST(ByteVectorTest, fromHex_rejectsInvalidHexDigit) {
|
||||
EXPECT_THROW(byte_vector::FromHex("12GG"), byte_vector::HexException);
|
||||
}
|
||||
|
||||
TEST(ByteVectorTest, fromHex_rejectsOddLengthInput) {
|
||||
EXPECT_THROW(byte_vector::FromHex("F"), byte_vector::HexException);
|
||||
EXPECT_THROW(byte_vector::FromHex("0x123"), byte_vector::HexException);
|
||||
}
|
||||
|
||||
TEST(ByteVectorTest, exposesOwnedByteAccessors) {
|
||||
byte_vector bytes({0x01, 0x02});
|
||||
|
||||
bytes.reserve(4);
|
||||
bytes += static_cast<uint8_t>(0x03);
|
||||
bytes.append("A", 1);
|
||||
|
||||
EXPECT_EQ(bytes.size(), 4U);
|
||||
EXPECT_FALSE(bytes.empty());
|
||||
EXPECT_EQ(bytes[0], 0x01);
|
||||
EXPECT_EQ(bytes[3], 'A');
|
||||
EXPECT_EQ(
|
||||
std::vector<uint8_t>(bytes.begin(), bytes.end()),
|
||||
std::vector<uint8_t>({1, 2, 3, 65}));
|
||||
}
|
||||
|
||||
TEST(ByteVectorTest, convertsToStringAndStreamsBytes) {
|
||||
const byte_vector bytes(std::string("abc"));
|
||||
|
||||
EXPECT_EQ(static_cast<std::string>(bytes), "abc");
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << bytes;
|
||||
EXPECT_EQ(stream.str(), "abc");
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
TEST(SequenceRandomGeneratorTests, testSequence) {
|
||||
@@ -22,3 +24,16 @@ TEST(SequenceRandomGeneratorTests, testSequence) {
|
||||
EXPECT_EQ(generator.Percentile(), 25.0);
|
||||
EXPECT_EQ(generator.Percentile(), 75.0);
|
||||
}
|
||||
|
||||
TEST(SequenceRandomGeneratorTests, rejectsEmptySequences) {
|
||||
EXPECT_THROW(SequenceRandomGenerator({}), std::invalid_argument);
|
||||
EXPECT_THROW(SequenceRandomGenerator::WithSequence({}), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(SequenceRandomGeneratorTests, rejectsValuesOutsideProbabilityRange) {
|
||||
EXPECT_THROW(SequenceRandomGenerator({-0.01}), std::invalid_argument);
|
||||
EXPECT_THROW(SequenceRandomGenerator({1.01}), std::invalid_argument);
|
||||
EXPECT_THROW(
|
||||
SequenceRandomGenerator({std::numeric_limits<double>::quiet_NaN()}),
|
||||
std::invalid_argument);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
|
||||
using std::vector;
|
||||
|
||||
class ConstantRandomGenerator final : public RandomGenerator {
|
||||
private:
|
||||
double value;
|
||||
|
||||
auto DoubleZeroToOne() -> double override { return value; }
|
||||
|
||||
public:
|
||||
explicit ConstantRandomGenerator(const double v) : value(v) {}
|
||||
};
|
||||
|
||||
TEST(StdLibraryGenerator, Percentile) {
|
||||
StdLibraryGenerator generator;
|
||||
|
||||
@@ -181,6 +191,42 @@ TEST(StdLibraryGenerator, IntBelow) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RandomGenerator, IntBetweenClampsEndpointOneToExclusiveUpperBound) {
|
||||
ConstantRandomGenerator generator(1.0);
|
||||
|
||||
EXPECT_EQ(9, generator.IntBetween(0, 10));
|
||||
EXPECT_EQ(4, generator.IntBetween(-5, 5));
|
||||
EXPECT_EQ(0, generator.IntBelow(1));
|
||||
}
|
||||
|
||||
TEST(RandomGenerator, RandomElementHandlesEndpointOneForSingleElementVector) {
|
||||
ConstantRandomGenerator generator(1.0);
|
||||
|
||||
EXPECT_EQ(42, generator.RandomElement(std::vector{42}));
|
||||
}
|
||||
|
||||
TEST(RandomGenerator, IntBetweenRejectsEmptyRanges) {
|
||||
ConstantRandomGenerator generator(0.5);
|
||||
|
||||
EXPECT_THROW(generator.IntBetween(3, 3), std::invalid_argument);
|
||||
EXPECT_THROW(generator.IntBetween(4, 3), std::invalid_argument);
|
||||
EXPECT_THROW(generator.IntBelow(0), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(StdLibraryGenerator, IntBetweenRejectsEmptyRanges) {
|
||||
StdLibraryGenerator generator;
|
||||
|
||||
EXPECT_THROW(generator.IntBetween(3, 3), std::invalid_argument);
|
||||
EXPECT_THROW(generator.IntBetween(4, 3), std::invalid_argument);
|
||||
EXPECT_THROW(generator.IntBelow(0), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(RandomGenerator, RandomElementRejectsEmptyVector) {
|
||||
ConstantRandomGenerator generator(0.5);
|
||||
|
||||
EXPECT_THROW(generator.RandomElement(std::vector<int>{}), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(RandomGenerator, chanceAtOrAbove) {
|
||||
EXPECT_DOUBLE_EQ(0.50, RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(50.0));
|
||||
EXPECT_DOUBLE_EQ(0.10, RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(90.0));
|
||||
|
||||
@@ -81,8 +81,8 @@ protected:
|
||||
TEST_F(AIAttackerStrategySelectorTest,
|
||||
CanClaimCastles_defenderOccupiesOne_returnsAttackCastlesStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
// 2 critical tiles, 3 attackers
|
||||
|
||||
// defender in critical tile
|
||||
@@ -108,8 +108,8 @@ TEST_F(AIAttackerStrategySelectorTest,
|
||||
|
||||
TEST_F(AIAttackerStrategySelectorTest, DefenderNotOnCastle_returnsAttackUnitsStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
// 2 critical tiles, 3 attackers
|
||||
|
||||
// defender not in critical tile - they're scattering, so chase them
|
||||
@@ -135,8 +135,8 @@ TEST_F(AIAttackerStrategySelectorTest, DefenderNotOnCastle_returnsAttackUnitsStr
|
||||
|
||||
TEST_F(AIAttackerStrategySelectorTest, NoDefendersRemaining_returnsHoldCastlesStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
// 2 critical tiles, 3 attackers
|
||||
|
||||
// defender has fled - no defenders remaining
|
||||
@@ -162,10 +162,10 @@ TEST_F(AIAttackerStrategySelectorTest, NoDefendersRemaining_returnsHoldCastlesSt
|
||||
|
||||
TEST_F(AIAttackerStrategySelectorTest, TooManyCastles_returnsAttackUnitsStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddIndex(3);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(3);
|
||||
// 4 critical tiles, 3 attackers
|
||||
|
||||
EXPECT_EQ(
|
||||
|
||||
@@ -79,10 +79,10 @@ protected:
|
||||
|
||||
TEST_F(AIStrategySelectorTest, NotEnoughAttackerUnits_returnsScatterStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddIndex(3);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(3);
|
||||
// 4 critical tiles, 3 attackers
|
||||
|
||||
EXPECT_EQ(
|
||||
@@ -100,10 +100,10 @@ TEST_F(AIStrategySelectorTest, NotEnoughAttackerUnits_returnsScatterStrategy) {
|
||||
|
||||
TEST_F(AIStrategySelectorTest, EnoughAttackerUnitsOnlyIncludingUndead_returnsScatterStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddIndex(3);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(3);
|
||||
// 4 critical tiles, 3 attackers
|
||||
|
||||
extraUnit0->mutate_player_id(0);
|
||||
@@ -125,10 +125,10 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnitsOnlyIncludingUndead_returnsSca
|
||||
|
||||
TEST_F(AIStrategySelectorTest, EnoughAttackerUnits_returnsHoldCastlesStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddIndex(3);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(3);
|
||||
// 4 critical tiles, 3 attackers
|
||||
|
||||
extraUnit0->mutate_player_id(0);
|
||||
@@ -151,10 +151,10 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnits_returnsHoldCastlesStrategy) {
|
||||
TEST_F(AIStrategySelectorTest,
|
||||
NotEnoughAttackerUnits_defenderOver60PercentOfAttacker_returnsHoldCastlesStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddIndex(3);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(3);
|
||||
// 4 critical tiles, 3 attackers
|
||||
|
||||
// 900 attackers, 541 defenders
|
||||
@@ -180,9 +180,9 @@ TEST_F(AIStrategySelectorTest,
|
||||
TEST_F(AIStrategySelectorTest,
|
||||
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_returnsFleeStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
// 3 critical tiles, 3 attackers
|
||||
|
||||
// 900 attackers, 134 defenders
|
||||
@@ -208,10 +208,10 @@ TEST_F(AIStrategySelectorTest,
|
||||
TEST_F(AIStrategySelectorTest,
|
||||
DefenderUnder15PercentOfAttacker_notEnoughAttackerUnits_returnsScatterStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddIndex(3);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(3);
|
||||
// 4 critical tiles, 3 attackers
|
||||
|
||||
// 900 attackers, 134 defenders
|
||||
@@ -237,9 +237,9 @@ TEST_F(AIStrategySelectorTest,
|
||||
TEST_F(AIStrategySelectorTest,
|
||||
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_cannotFlee_returnsScatterStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
// 3 critical tiles, 3 attackers
|
||||
|
||||
// 900 attackers, 134 defenders
|
||||
@@ -266,9 +266,9 @@ TEST_F(AIStrategySelectorTest,
|
||||
TEST_F(AIStrategySelectorTest,
|
||||
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_lateGame_returnsScatterStrategy) {
|
||||
CoordsSet criticalTileCoords(6, 6);
|
||||
criticalTileCoords.AddIndex(0);
|
||||
criticalTileCoords.AddIndex(1);
|
||||
criticalTileCoords.AddIndex(2);
|
||||
criticalTileCoords.AddMapIndexUnchecked(0);
|
||||
criticalTileCoords.AddMapIndexUnchecked(1);
|
||||
criticalTileCoords.AddMapIndexUnchecked(2);
|
||||
// 3 critical tiles, 3 attackers
|
||||
|
||||
// 900 attackers, 134 defenders
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace shardok {
|
||||
* @param defenderToggle If true, AI is defender. If false, AI is attacker.
|
||||
* @return GameStateW in SETUP phase with 6 units per player in reserve
|
||||
*/
|
||||
auto CreatePerfTestGameState(const GameSettingsSPtr& settings, bool defenderToggle) -> GameStateW;
|
||||
[[nodiscard]] auto CreatePerfTestGameState(const GameSettingsSPtr& settings, bool defenderToggle)
|
||||
-> GameStateW;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = ["//src/test/cpp/net/eagle0/shardok:__subpackages__"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:hex_map_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
|
||||
@@ -26,10 +26,11 @@ using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
using shardok::PlayerId;
|
||||
using shardok::UnitId;
|
||||
|
||||
auto AddPlayerInfo(flatbuffers::FlatBufferBuilder &fbb, int playerId, bool isDefender, int food)
|
||||
[[nodiscard]] auto
|
||||
AddPlayerInfo(flatbuffers::FlatBufferBuilder &fbb, int playerId, bool isDefender, int food)
|
||||
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
|
||||
|
||||
auto AddPlayerInfo(
|
||||
[[nodiscard]] auto AddPlayerInfo(
|
||||
flatbuffers::FlatBufferBuilder &fbb,
|
||||
int playerId,
|
||||
bool isDefender,
|
||||
@@ -37,7 +38,7 @@ auto AddPlayerInfo(
|
||||
std::vector<int8_t> victoryConditions)
|
||||
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
|
||||
|
||||
auto AddPlayerInfo(
|
||||
[[nodiscard]] auto AddPlayerInfo(
|
||||
flatbuffers::FlatBufferBuilder &fbb,
|
||||
int playerId,
|
||||
bool isDefender,
|
||||
@@ -46,30 +47,30 @@ auto AddPlayerInfo(
|
||||
std::vector<int> allyPids)
|
||||
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
|
||||
|
||||
auto AddGenericUnit(
|
||||
[[nodiscard]] auto AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords &location) -> Unit;
|
||||
|
||||
auto AddGenericUnit(
|
||||
[[nodiscard]] auto AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords &location,
|
||||
void (*modifier)(net::eagle0::shardok::storage::fb::Unit &)) -> Unit;
|
||||
|
||||
auto AddGenericUnit(
|
||||
[[nodiscard]] auto AddGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords &location,
|
||||
net::eagle0::shardok::storage::fb::Profession profession,
|
||||
void (*unitModifier)(net::eagle0::shardok::storage::fb::Unit &)) -> Unit;
|
||||
|
||||
auto AddGenericUndead(
|
||||
[[nodiscard]] auto AddGenericUndead(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
const net::eagle0::shardok::storage::fb::Coords &location) -> Unit;
|
||||
|
||||
auto MakeGenericUnit(
|
||||
[[nodiscard]] auto MakeGenericUnit(
|
||||
PlayerId playerId,
|
||||
UnitId unitId,
|
||||
net::eagle0::shardok::storage::fb::Coords location) -> Unit;
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
#include "GtestExtensions.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
|
||||
class NotFoundException : public std::exception {
|
||||
private:
|
||||
const std::string message;
|
||||
@@ -21,22 +25,37 @@ auto ResultWithType(const std::vector<ActionResult>& results, ActionType type) -
|
||||
return *result;
|
||||
}
|
||||
|
||||
auto DecodeUnitBytesForTest(const std::string& unitBytes) -> Unit {
|
||||
return shardok::UnitFromBytes(unitBytes);
|
||||
}
|
||||
|
||||
auto DecodeResolvedUnitForTest(const ResolvedUnit& resolvedUnit) -> Unit {
|
||||
return DecodeUnitBytesForTest(resolvedUnit.unit_bytes());
|
||||
}
|
||||
|
||||
auto DecodedChangedUnitsForTest(const ActionResult& result) -> std::vector<Unit>& {
|
||||
thread_local std::unordered_map<const ActionResult*, std::vector<Unit>> decodedByResult;
|
||||
auto& units = decodedByResult[&result];
|
||||
units.clear();
|
||||
units.reserve(result.changed_units_fb_size());
|
||||
for (const auto& unitBytes : result.changed_units_fb()) {
|
||||
units.push_back(DecodeUnitBytesForTest(unitBytes));
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
auto ChangedUnitFbWithId(const ActionResult& result, const int id) -> Unit* {
|
||||
const int changedUnitCount = result.changed_units_fb_size();
|
||||
for (int i = 0; i < changedUnitCount; ++i) {
|
||||
auto* unit =
|
||||
const_cast<Unit*>(reinterpret_cast<const Unit*>(result.changed_units_fb(i).data()));
|
||||
if (unit->unit_id() == id) return unit;
|
||||
auto& units = DecodedChangedUnitsForTest(result);
|
||||
for (auto& unit : units) {
|
||||
if (unit.unit_id() == id) return &unit;
|
||||
}
|
||||
throw NotFoundException("No such changed id " + std::to_string(id));
|
||||
}
|
||||
|
||||
auto MaybeChangedUnitFbWithId(const ActionResult& result, const int id) -> Unit* {
|
||||
const int changedUnitCount = result.changed_units_fb_size();
|
||||
for (int i = 0; i < changedUnitCount; ++i) {
|
||||
auto* unit =
|
||||
const_cast<Unit*>(reinterpret_cast<const Unit*>(result.changed_units_fb(i).data()));
|
||||
if (unit->unit_id() == id) return unit;
|
||||
auto& units = DecodedChangedUnitsForTest(result);
|
||||
for (auto& unit : units) {
|
||||
if (unit.unit_id() == id) return &unit;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
@@ -25,22 +26,26 @@
|
||||
|
||||
using net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
auto operator==(
|
||||
[[nodiscard]] auto operator==(
|
||||
const net::eagle0::shardok::common::TileModifier_SingleModifier& lhs,
|
||||
const net::eagle0::shardok::common::TileModifier_SingleModifier& rhs) -> bool;
|
||||
auto operator==(
|
||||
[[nodiscard]] auto operator==(
|
||||
const net::eagle0::shardok::common::TileModifierWithCoords& lhs,
|
||||
const net::eagle0::shardok::common::TileModifierWithCoords& rhs) -> bool;
|
||||
|
||||
using net::eagle0::shardok::common::ActionType;
|
||||
using net::eagle0::shardok::storage::ActionResult;
|
||||
using ResolvedUnit = net::eagle0::shardok::storage::ResolvedUnit;
|
||||
|
||||
auto ResultWithType(const std::vector<ActionResult>& results, ActionType type) -> ActionResult;
|
||||
auto ChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
|
||||
auto MaybeChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
|
||||
[[nodiscard]] auto ResultWithType(const std::vector<ActionResult>& results, ActionType type)
|
||||
-> ActionResult;
|
||||
[[nodiscard]] auto DecodeUnitBytesForTest(const std::string& unitBytes) -> Unit;
|
||||
[[nodiscard]] auto DecodeResolvedUnitForTest(const ResolvedUnit& resolvedUnit) -> Unit;
|
||||
[[nodiscard]] auto ChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
|
||||
[[nodiscard]] auto MaybeChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
|
||||
|
||||
template<class T, class VecT>
|
||||
auto CHECK_CONTAINS(const VecT& vec, const T& obj) -> bool {
|
||||
[[nodiscard]] auto CHECK_CONTAINS(const VecT& vec, const T& obj) -> bool {
|
||||
// Use member function Contains if available (for custom types like CoordsSet)
|
||||
if constexpr (requires { vec.Contains(obj); }) {
|
||||
return vec.Contains(obj);
|
||||
@@ -51,7 +56,7 @@ auto CHECK_CONTAINS(const VecT& vec, const T& obj) -> bool {
|
||||
}
|
||||
|
||||
template<class T, class VecT>
|
||||
auto CHECK_NOCONTAINS(const VecT& vec, const T& obj) -> bool {
|
||||
[[nodiscard]] auto CHECK_NOCONTAINS(const VecT& vec, const T& obj) -> bool {
|
||||
return !CHECK_CONTAINS(vec, obj);
|
||||
}
|
||||
|
||||
@@ -66,7 +71,7 @@ void EXPECT_CONTAINS(const VecT& vec, const T& obj) {
|
||||
}
|
||||
|
||||
template<class VecT, class F>
|
||||
auto CHECK_NOCONTAINS_WHERE(const VecT& vec, F where) -> bool {
|
||||
[[nodiscard]] auto CHECK_NOCONTAINS_WHERE(const VecT& vec, F where) -> bool {
|
||||
for (size_t i = 0; i < vec.size(); ++i) {
|
||||
if (where(vec[i])) {
|
||||
std::cout << "Found at index " << i << '\n';
|
||||
@@ -77,12 +82,12 @@ auto CHECK_NOCONTAINS_WHERE(const VecT& vec, F where) -> bool {
|
||||
}
|
||||
|
||||
template<class T, class F>
|
||||
auto GET_RESULT_WHERE(const std::vector<T>& vec, F where) -> T {
|
||||
[[nodiscard]] auto GET_RESULT_WHERE(const std::vector<T>& vec, F where) -> T {
|
||||
return *std::ranges::find_if(vec, where);
|
||||
}
|
||||
|
||||
template<class T, class F>
|
||||
auto CHECK_CONTAINS_WHERE(T& vec, F where) -> bool {
|
||||
[[nodiscard]] auto CHECK_CONTAINS_WHERE(T& vec, F where) -> bool {
|
||||
return std::ranges::find_if(vec, where) != vec.end();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ constexpr int8_t HERO1_BRV = 90.0;
|
||||
constexpr net::eagle0::shardok::storage::fb::Profession HEROT1_PROFESSION =
|
||||
net::eagle0::shardok::storage::fb::Profession_NO_PROFESSION;
|
||||
|
||||
static inline auto GetHeroT1() -> net::eagle0::shardok::storage::fb::Hero {
|
||||
[[nodiscard]] static inline auto GetHeroT1() -> net::eagle0::shardok::storage::fb::Hero {
|
||||
net::eagle0::shardok::storage::fb::Hero hero;
|
||||
|
||||
hero.mutate_eagle_hero_id(1);
|
||||
@@ -74,7 +74,7 @@ constexpr int8_t HERO2_BRV = 100.0;
|
||||
constexpr net::eagle0::shardok::storage::fb::Profession HEROT2_PROFESSION =
|
||||
net::eagle0::shardok::storage::fb::Profession_NO_PROFESSION;
|
||||
|
||||
inline auto GetHeroT2() -> net::eagle0::shardok::storage::fb::Hero {
|
||||
[[nodiscard]] inline auto GetHeroT2() -> net::eagle0::shardok::storage::fb::Hero {
|
||||
net::eagle0::shardok::storage::fb::Hero hero;
|
||||
|
||||
hero.mutate_eagle_hero_id(2);
|
||||
|
||||
@@ -23,15 +23,17 @@ using Terrain = net::eagle0::shardok::storage::fb::Terrain;
|
||||
|
||||
using TerrainProto = net::eagle0::shardok::common::Terrain;
|
||||
|
||||
static inline auto MakeCoordsSet(const HexMap* map, const std::initializer_list<Coords> list)
|
||||
-> CoordsSet {
|
||||
[[nodiscard]] static inline auto MakeCoordsSet(
|
||||
const HexMap* map,
|
||||
const std::initializer_list<Coords> list) -> CoordsSet {
|
||||
CoordsSet cs(map);
|
||||
for (const Coords& c : list) { cs.Add(c); }
|
||||
return cs;
|
||||
}
|
||||
|
||||
static inline auto MakeCoordsSet(const HexMapProto& map, const std::initializer_list<Coords> list)
|
||||
-> CoordsSet {
|
||||
[[nodiscard]] static inline auto MakeCoordsSet(
|
||||
const HexMapProto& map,
|
||||
const std::initializer_list<Coords> list) -> CoordsSet {
|
||||
CoordsSet cs(map.row_count(), map.column_count());
|
||||
for (const Coords& c : list) { cs.Add(c); }
|
||||
return cs;
|
||||
@@ -62,13 +64,14 @@ static vector<vector<Coords>> ATTACKER_STARTING_POSITIONS = {
|
||||
/* 3 */ {Coords(0, 5), Coords(0, 4), Coords(1, 5)},
|
||||
/* 4 */ {}};
|
||||
|
||||
static auto MakeSunnyWeather() -> MonthlyWeatherProto {
|
||||
[[nodiscard]] static auto MakeSunnyWeather() -> MonthlyWeatherProto {
|
||||
MonthlyWeatherProto mw;
|
||||
mw.set_sun_chance(100);
|
||||
mw.set_average_temperature(60);
|
||||
return mw;
|
||||
}
|
||||
static auto MakeSunnyWeatherFb() -> net::eagle0::shardok::storage::fb::MonthlyWeather {
|
||||
[[nodiscard]] static auto MakeSunnyWeatherFb()
|
||||
-> net::eagle0::shardok::storage::fb::MonthlyWeather {
|
||||
net::eagle0::shardok::storage::fb::MonthlyWeather mw;
|
||||
mw.mutate_sun_chance(100);
|
||||
mw.mutate_average_temperature(60);
|
||||
@@ -80,13 +83,14 @@ static vector<MonthlyWeatherProto> SUNNY_MONTHLY_WEATHERS =
|
||||
static vector<net::eagle0::shardok::storage::fb::MonthlyWeather> SUNNY_MONTHLY_WEATHERS_FB =
|
||||
vector<net::eagle0::shardok::storage::fb::MonthlyWeather>(12, MakeSunnyWeatherFb());
|
||||
|
||||
static inline auto MutableTerrain(HexMap* map, const Coords& position) -> Terrain* {
|
||||
[[nodiscard]] static inline auto MutableTerrain(HexMap* map, const Coords& position) -> Terrain* {
|
||||
// const_cast is safe because we're accessing through a mutable HexMap pointer
|
||||
return const_cast<Terrain*>(map->mutable_terrain()->GetMutableObject(
|
||||
position.row() * map->column_count() + position.column()));
|
||||
}
|
||||
|
||||
static inline auto MutableTerrain(HexMapProto& map, const Coords& position) -> TerrainProto* {
|
||||
[[nodiscard]] static inline auto MutableTerrain(HexMapProto& map, const Coords& position)
|
||||
-> TerrainProto* {
|
||||
return map.mutable_terrain(position.row() * map.column_count() + position.column());
|
||||
}
|
||||
|
||||
@@ -106,7 +110,8 @@ static auto MAP_WITH_STARTING_POSITIONS_FB = MakeHexMap(
|
||||
ATTACKER_STARTING_POSITIONS,
|
||||
SUNNY_MONTHLY_WEATHERS_FB);
|
||||
|
||||
static inline auto AddBasicMap(flatbuffers::FlatBufferBuilder& fbb) -> flatbuffers::Offset<HexMap> {
|
||||
[[nodiscard]] static inline auto AddBasicMap(flatbuffers::FlatBufferBuilder& fbb)
|
||||
-> flatbuffers::Offset<HexMap> {
|
||||
return AddHexMap(
|
||||
fbb,
|
||||
BASIC_MAP_FB->row_count(),
|
||||
|
||||
@@ -28,10 +28,10 @@ using HexMapProto = net::eagle0::shardok::common::HexMap;
|
||||
using MonthlyWeatherProto = net::eagle0::shardok::common::MonthlyWeather;
|
||||
using TerrainTypeProto = net::eagle0::shardok::common::Terrain::Type;
|
||||
|
||||
auto AddHexMap(flatbuffers::FlatBufferBuilder &fbb, const HexMap *hm)
|
||||
[[nodiscard]] auto AddHexMap(flatbuffers::FlatBufferBuilder &fbb, const HexMap *hm)
|
||||
-> flatbuffers::Offset<HexMap>;
|
||||
|
||||
auto AddHexMap(
|
||||
[[nodiscard]] auto AddHexMap(
|
||||
flatbuffers::FlatBufferBuilder &fbb,
|
||||
MapIndex rowCount,
|
||||
MapIndex columnCount,
|
||||
@@ -43,7 +43,7 @@ auto AddHexMap(
|
||||
|
||||
void EmplaceHexMap(GameStateW &gameState, const HexMap *hexMap);
|
||||
|
||||
auto MakeHexMap(
|
||||
[[nodiscard]] auto MakeHexMap(
|
||||
MapIndex rowCount,
|
||||
MapIndex columnCount,
|
||||
const std::vector<net::eagle0::shardok::storage::fb::Terrain_::Type> &terrainTypes,
|
||||
@@ -52,7 +52,7 @@ auto MakeHexMap(
|
||||
const std::vector<net::eagle0::shardok::storage::fb::MonthlyWeather> &monthlyWeathers)
|
||||
-> HexMapWrapper;
|
||||
|
||||
auto MakeHexMapProto(
|
||||
[[nodiscard]] auto MakeHexMapProto(
|
||||
int rowCount,
|
||||
int columnCount,
|
||||
const std::vector<TerrainTypeProto> &terrainTypes,
|
||||
|
||||
@@ -241,7 +241,7 @@ auto GameWithHiddenDefenders(
|
||||
allStartingUnits,
|
||||
weather,
|
||||
fireTiles,
|
||||
CoordsSet(map.row_count(), map.column_count()),
|
||||
CoordsSet(map.column_count(), map.row_count()),
|
||||
{},
|
||||
gen);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ extern PlayerInfoProto ATTACKER_INFO;
|
||||
extern PlayerInfoProto DEFENDER_INFO;
|
||||
extern std::vector<PlayerInfoProto> PLAYER_INFOS;
|
||||
|
||||
auto GameWithUnplacedUnits(const HexMapProto &map, std::vector<Unit> &unplacedUnits)
|
||||
[[nodiscard]] auto GameWithUnplacedUnits(const HexMapProto &map, std::vector<Unit> &unplacedUnits)
|
||||
-> std::shared_ptr<ShardokEngine>;
|
||||
|
||||
auto GameWithHiddenDefenders(
|
||||
[[nodiscard]] auto GameWithHiddenDefenders(
|
||||
const HexMapProto &map,
|
||||
std::vector<Unit> &attackingUnits,
|
||||
std::vector<Unit> &hiddenDefenders,
|
||||
@@ -57,7 +57,7 @@ auto GameWithHiddenDefenders(
|
||||
const CoordsSet &fireTiles = CoordsSet(6, 6),
|
||||
const std::shared_ptr<RandomGenerator> &gen = nullptr) -> std::shared_ptr<ShardokEngine>;
|
||||
|
||||
auto GameWithUnits(
|
||||
[[nodiscard]] auto GameWithUnits(
|
||||
const HexMapProto &map,
|
||||
std::vector<Unit> &attackingUnitsWithDefaultIds,
|
||||
std::vector<Unit> &defendingUnitsWithDefaultIds,
|
||||
@@ -69,7 +69,7 @@ auto GameWithUnits(
|
||||
const std::vector<std::pair<Coords, double>> &bridgedTiles = {})
|
||||
-> std::shared_ptr<ShardokEngine>;
|
||||
|
||||
auto GameWithUnitsAndReservedSlots(
|
||||
[[nodiscard]] auto GameWithUnitsAndReservedSlots(
|
||||
const HexMapProto &map,
|
||||
std::vector<Unit> &attackingUnitsWithDefaultIds,
|
||||
std::vector<Unit> &defendingUnitsWithDefaultIds,
|
||||
@@ -132,6 +132,6 @@ auto postEndTurnCommand(
|
||||
std::shared_ptr<RandomGenerator> generator) -> std::vector<ActionResultView>;
|
||||
|
||||
// Load a game state from a binary file dumped by ShardokAIClient
|
||||
auto LoadGameStateFromFile(const std::string &path) -> GameStateW;
|
||||
[[nodiscard]] auto LoadGameStateFromFile(const std::string &path) -> GameStateW;
|
||||
|
||||
#endif // EAGLE0_TEST_SHARDOK_LIBRARY_SHARDOK_ENGINE_BASED_TEST_DATA_HPP
|
||||
|
||||
@@ -291,7 +291,7 @@ TEST_F(ShardokEngineTests, GetEligibleCharger_afterCharging_returnsAbsent) {
|
||||
postCommandWithTypeAndTarget(game, 0, 0, MOVE_COMMAND, Coords(2, 1));
|
||||
vector<ActionResultView> results;
|
||||
results = postCommandWithTypeAndTarget(game, 0, 0, CHARGE_COMMAND, Coords(2, 0));
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
|
||||
EXPECT_EQ(-1, game->GetCurrentGameState()->eligible_charger_id());
|
||||
}
|
||||
@@ -312,11 +312,11 @@ TEST_F(ShardokEngineTests, GetEligibleCharger_afterRestingHero_returnsAbsent) {
|
||||
|
||||
vector<ActionResultView> results;
|
||||
results = postCommandWithTypeAndTarget(game, 0, 0, MOVE_COMMAND, Coords(2, 1));
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
EXPECT_NE(-1, game->GetCurrentGameState()->eligible_charger_id());
|
||||
|
||||
results = postCommandWithType(game, 0, 0, UNIT_REST_COMMAND);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
|
||||
EXPECT_EQ(-1, game->GetCurrentGameState()->eligible_charger_id());
|
||||
}
|
||||
@@ -339,11 +339,11 @@ TEST_F(ShardokEngineTests, GetEligibleCharger_afterStoppingHero_returnsAbsent) {
|
||||
|
||||
vector<ActionResultView> results;
|
||||
results = postCommandWithTypeAndTarget(game, 0, 0, MOVE_COMMAND, Coords(2, 1));
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
EXPECT_NE(-1, game->GetCurrentGameState()->eligible_charger_id());
|
||||
|
||||
results = postCommandWithType(game, 0, 0, UNIT_STOP_COMMAND);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
|
||||
EXPECT_EQ(-1, game->GetCurrentGameState()->eligible_charger_id());
|
||||
}
|
||||
@@ -364,7 +364,7 @@ TEST_F(ShardokEngineTests, placeHero_placesHero) {
|
||||
0,
|
||||
PLACE_UNIT_COMMAND,
|
||||
ATTACKER_STARTING_POSITIONS[0][0]);
|
||||
EXPECT_EQ(1u, results.size());
|
||||
EXPECT_EQ(1U, results.size());
|
||||
|
||||
const ActionResultView result = results[0];
|
||||
|
||||
@@ -399,14 +399,14 @@ TEST_F(ShardokEngineTests, placeLastHero_switchesPlayer) {
|
||||
0,
|
||||
PLACE_UNIT_COMMAND,
|
||||
ATTACKER_STARTING_POSITIONS[0][0]);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
results = postCommandWithTypeAndTarget(
|
||||
game,
|
||||
0,
|
||||
1,
|
||||
PLACE_UNIT_COMMAND,
|
||||
ATTACKER_STARTING_POSITIONS[0][1]);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
// post the end setup command
|
||||
postWithResults(game, 0, 0);
|
||||
|
||||
@@ -432,14 +432,14 @@ TEST_F(ShardokEngineTests, placeLastHeroBothPlayers_startsGame) {
|
||||
0,
|
||||
PLACE_UNIT_COMMAND,
|
||||
ATTACKER_STARTING_POSITIONS[0][0]);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
results = postCommandWithTypeAndTarget(
|
||||
game,
|
||||
0,
|
||||
1,
|
||||
PLACE_UNIT_COMMAND,
|
||||
ATTACKER_STARTING_POSITIONS[0][1]);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
|
||||
// post the end setup command
|
||||
postWithResults(game, 0, 0);
|
||||
@@ -450,7 +450,7 @@ TEST_F(ShardokEngineTests, placeLastHeroBothPlayers_startsGame) {
|
||||
2,
|
||||
PLACE_UNIT_COMMAND,
|
||||
DEFENDER_STARTING_POSITIONS[0]);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
|
||||
// post the end setup command
|
||||
results = postWithResults(game, 1, 0);
|
||||
@@ -663,7 +663,7 @@ TEST_F(ShardokEngineTests, GetGameHistory_startMeteorLowKnowledge_filteredOut) {
|
||||
const auto game = GameWithUnits(BASIC_MAP, attackerUnits, defenderUnits);
|
||||
|
||||
const auto results = postCommandWithType(game, 0, 0, METEOR_START_COMMAND);
|
||||
EXPECT_NE(0u, results.size());
|
||||
EXPECT_NE(0U, results.size());
|
||||
|
||||
auto history = game->GetFilteredGameHistory(0);
|
||||
EXPECT_CONTAINS_WHERE(history, [](const ActionResultView &result) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user