From e0ee76ba19ba5e601d84bd25151e45e0d95a869b Mon Sep 17 00:00:00 2001 From: Natalie Rose Date: Fri, 4 Sep 2026 14:49:54 +1000 Subject: [PATCH] AI empire and Lacuna-Server-Private code fixes --- bin/cult | 1 - bin/cult/add_colonies.pl | 200 ++++++++++++++++++ bin/cult/hourly_update.pl | 42 ++++ bin/delambert/add_colonies.pl | 1 + bin/deploy.pl | 21 +- bin/deploy.psgi | 59 +++++- bin/lacuna.logrotate | 52 ++++- bin/startqa.sh | 22 +- lib/Lacuna/AI/Cult.pm | 8 + lib/Lacuna/AI/DeLambert.pm | 153 +++++++++++++- lib/Lacuna/DB/Result/AIBattleSummary.pm | 15 +- lib/Lacuna/DB/Result/AIScratchPad.pm | 8 +- lib/Lacuna/DB/Result/Log/Battles.pm | 15 +- lib/Lacuna/DB/Result/Ships.pm | 17 +- lib/Lacuna/Role/Ship/Arrive/TriggerDefense.pm | 2 +- var/upgrades/4.0002.sql | 25 +++ 16 files changed, 604 insertions(+), 37 deletions(-) delete mode 120000 bin/cult create mode 100644 bin/cult/add_colonies.pl create mode 100644 bin/cult/hourly_update.pl create mode 100644 var/upgrades/4.0002.sql diff --git a/bin/cult b/bin/cult deleted file mode 120000 index a8649aa5..00000000 --- a/bin/cult +++ /dev/null @@ -1 +0,0 @@ -/data/Lacuna-Server-Private/bin/cult \ No newline at end of file diff --git a/bin/cult/add_colonies.pl b/bin/cult/add_colonies.pl new file mode 100644 index 00000000..97ef7046 --- /dev/null +++ b/bin/cult/add_colonies.pl @@ -0,0 +1,200 @@ +use 5.010; +use strict; +use warnings; +use lib '/home/lacuna/server/lib'; + +use Lacuna::DB; +use Lacuna; +use Lacuna::Util qw(randint format_date random_element); + +use Getopt::Long; +use List::MoreUtils qw(uniq); +use Data::Dumper; + +$|=1; +our $quiet = 0; # omit output messages +our $respawn = 0; # delete and respawn the empire + +GetOptions( + 'quiet' => \$quiet, + 'respawn' => \$respawn, +); + +out('Started'); +my $start = time; + +out('Loading DB'); +our $db = Lacuna->db; +my $config = Lacuna->config; +my $empires = $db->resultset('Lacuna::DB::Result::Empire'); +my $empire; + +if ($respawn) { + # with 'respawn' we delete and re-create the whole empire + out('Re-Spawning Empire'); + + $empire = $empires->find(-5); + + if (defined $empire) { + out('Deleting existing empire'); + # First ensure we have demolished all glyph resource buildings + for my $planet ($empire->planets->all) { + out("Removing sensitive buildings from ".$planet->name); + $planet->delete_buildings($planet->building_cache); + out("Renaming"); + + # Rename the planet + my $pname = $planet->star->name." ".$planet->orbit; + my $orbit = 8; + my $test; + do { + $test = $db->resultset('Lacuna::DB::Result::Map::Body')->search({ + name => $pname })->first; + if ($test) { + $orbit++; + $pname = $planet->star->name." ".$orbit; + } + } while ($test); + $planet->name($pname); + $planet->update; + out("Done with ".$planet->name); + } + + $empire->delete; + } +} + +$empire = $empires->find(-5); +if (not defined $empire) { + out('Creating new empire'); + $empire = create_empire(); +} + +my $finish = time; +out('Finished'); +out((int(($finish - $start)/60*100)/100)." minutes have elapsed"); + +############### +## SUBROUTINES +############### + +sub out { + my $message = shift; + unless ($quiet) { + say format_date(DateTime->now), " ", $message; + } +} + +sub create_empire { + out('Creating empire...'); + my $empire = Lacuna->db->resultset('Lacuna::DB::Result::Empire')->new({ + id => -5, + name => 'Cult of the Fissure', + stage => 'turing', + date_created => DateTime->now, + status_message => 'All is to be part of the Void.', + description => 'Break on thru to the other side', + password => Lacuna::DB::Result::Empire->encrypt_password(rand(99999999)), + species_name => 'cultists', + species_description => 'Only by releasing the fissures can we free ourselves', + essentia => 100, + min_orbit => 3, + max_orbit => 3, + manufacturing_affinity => 1, # cost of building new stuff + deception_affinity => 1, # spying ability + research_affinity => 1, # cost of upgrading + management_affinity => 1, # speed to build + farming_affinity => 1, # food + mining_affinity => 1, # minerals + science_affinity => 1, # energy, propultion, and other tech + environmental_affinity => 1, # waste and water + political_affinity => 1, # happiness + trade_affinity => 1, # speed of cargoships, and amount of cargo hauled + growth_affinity => 1, # price and speed of colony ships, and planetary command center start level + }); + + out('Find home planet...'); + my @bodies = $db->resultset('Map::Body')->search({ + 'me.empire_id' => undef, + 'stars.station_id' => undef, + 'me.class' => { like => 'Lacuna::DB::Result::Map::Body::Planet::P%' }, + 'me.orbit' => { between => [$empire->min_orbit, $empire->max_orbit] }, + },{ + join => 'stars', + rows => 100, + order_by => 'me.name', + }); + my $home = random_element(\@bodies); + + $empire->insert; + $home->delete_buildings($home->building_cache); + $empire->found($home); + $empire->university_level(30); + $empire->update; + create_colony($home); + + return $empire; +} + + +sub create_colony { + my ($body) = @_; + + out("Creating Cult Colony on body ".$body->name); + $body->name("Blue Oyster"); + $body->update; + + out('Upgrading PCC'); + my $pcc = $body->command; + $pcc->level(30); + $pcc->update; + + my $has_buildings = { + 'Waste::Sequestration' => {qty => 1, level => 30}, + 'Intelligence' => {qty => 1, level => 20}, + 'Security' => {qty => 1, level => 20}, + 'Espionage' => {qty => 1, level => 20}, + 'Shipyard' => {qty => 1, level => 10}, + 'SpacePort' => {qty => 4, level => 20}, + 'Observatory' => {qty => 1, level => 10}, + 'Archaeology' => {qty => 1, level => 10}, + 'Trade' => {qty => 1, level => 10}, + 'SAW' => {qty => 6, level => 20}, + 'Water::Storage' => {qty => 1, level => 30}, + 'Ore::Storage' => {qty => 1, level => 30}, + 'Energy::Reserve' => {qty => 1, level => 30}, + 'Food::Reserve' => {qty => 1, level => 30}, + 'Food::Corn' => {qty => 1, level => 15}, + 'Food::Wheat' => {qty => 1, level => 15}, + 'Food::Dairy' => {qty => 1, level => 15}, + 'Permanent::Volcano' => {qty => 1, level => 25}, + 'Permanent::NaturalSpring' => {qty => 1, level => 25}, + 'Permanent::InterDimensionalRift' => {qty => 1, level => 25}, + 'Permanent::GeoThermalVent' => {qty => 1, level => 25}, + 'Permanent::KalavianRuins' => {qty => 1, level => 10}, + 'Permanent::MalcudField' => {qty => 1, level => 24}, + 'Permanent::AlgaePond' => {qty => 1, level => 24}, + 'Permanent::BlackHoleGenerator' => {qty => 1, level => 30}, + 'Permanent::Ravine' => {qty => 1, level => 30}, + 'Permanent::TerraformingPlatform' => {qty => 5, level => 10}, + }; + + my $buildings = $db->resultset('Lacuna::DB::Result::Building'); + my $to_build = $has_buildings; + + foreach my $plan (keys %$to_build) { + for (1..$to_build->{$plan}{qty}) { + my ($x, $y) = $body->find_free_space; + my $building = $buildings->new({ + class => "Lacuna::DB::Result::Building::$plan", + level => $to_build->{$plan}{level} - 1, + x => $x, + y => $y, + body_id => $body->id, + body => $body, + }); + $body->build_building($building); + $building->finish_upgrade; + } + } +} diff --git a/bin/cult/hourly_update.pl b/bin/cult/hourly_update.pl new file mode 100644 index 00000000..6b644972 --- /dev/null +++ b/bin/cult/hourly_update.pl @@ -0,0 +1,42 @@ +use 5.010; +use strict; +use lib '/home/lacuna/server/lib'; +use Lacuna::DB; +use Lacuna; +use Lacuna::Util qw(format_date); +use Getopt::Long; +$|=1; +our $quiet; +GetOptions( + 'quiet' => \$quiet, +); + +out('Started'); +my $start = time; + +out('Loading AI'); +my $ai = Lacuna::AI::Cult->new; + +out('Running Updates'); +$ai->run_all_hourly_colony_updates; + + +my $finish = time; +out('Finished'); +out((($finish - $start)/60)." minutes have elapsed"); + + + + +############### +## SUBROUTINES +############### + + + +sub out { + my $message = shift; + unless ($quiet) { + say format_date(DateTime->now), " ", $message; + } +} diff --git a/bin/delambert/add_colonies.pl b/bin/delambert/add_colonies.pl index 808ba016..b33f39bd 100644 --- a/bin/delambert/add_colonies.pl +++ b/bin/delambert/add_colonies.pl @@ -231,6 +231,7 @@ sub create_empire { political_affinity => 1, # happiness trade_affinity => 7, # speed of cargoships, and amount of cargo hauled growth_affinity => 1, # price and speed of colony ships, and planetary command center start level + is_isolationist => 0, # DeLamberti trade with everyone - ships must be able to reach them }); out('Find home planet...'); diff --git a/bin/deploy.pl b/bin/deploy.pl index d5e70cea..e1b29f3d 100755 --- a/bin/deploy.pl +++ b/bin/deploy.pl @@ -19,6 +19,7 @@ BEGIN { } } +use FindBin; use Config::JSON; use Symbol; use IPC::Open3; @@ -156,23 +157,19 @@ END_TEXT } elsif ($repo eq 'Lacuna-Server-Open') { my $restart_server = 0; - # Reboot code - if ($branch eq "pt-reboot") { - chdir('/data/Lacuna-Server'); - system("git", "pull", "origin", "pt-reboot"); - $restart_server = 1; - } + my $server_path = $repo_config->{path} || "$FindBin::Bin/.."; - # Legacy code - elsif ($branch eq "pt") { - chdir('/data/Lacuna-Server-Open'); - system("git", "pull", "origin", "pt"); + if ($branch eq "pt-reboot" or $branch eq "pt") { + chdir($server_path); + system("git", "pull", "origin", $branch); $restart_server = 1; } if ($restart_server) { - chdir('/data/Lacuna-Server-Private/bin'); - system("./startqa.sh"); + # startqa.sh used to live in the (now gone) Lacuna-Server-Private + # repo; it is now a normal script alongside this one. + chdir($FindBin::Bin); + system("$FindBin::Bin/startqa.sh"); } } elsif ($repo eq 'Lacuna-Assets') { diff --git a/bin/deploy.psgi b/bin/deploy.psgi index 1c10fca6..bd5a3b40 120000 --- a/bin/deploy.psgi +++ b/bin/deploy.psgi @@ -1 +1,58 @@ -../../Lacuna-Server-Private/bin/deploy.psgi \ No newline at end of file +use strict; +use 5.010; + +# +# Deploy webhook front-end for The Lacuna Expanse server. +# +# Reconstructed: this file only ever existed in this repo as a symlink into the +# (now gone) Lacuna-Server-Private repo. bin/start_deploy.sh / bin/start_pt.sh +# run it under starman on port 5001. It receives a git host push notification +# and hands ($repo, $branch) to bin/deploy.pl, which does the actual pull / +# asset publish / server restart. +# +# The GitHub-push-based deploy flow this fronted is retired (deployment is now +# Docker Compose), so this is intentionally a thin shim: enough to keep the +# start scripts working and to still allow a manual kick. +# + +use FindBin; +use JSON qw(decode_json); +use Plack::Request; + +my $deploy_script = "$FindBin::Bin/deploy.pl"; + +no warnings 'void'; + +sub dispatch { + my ($repo, $branch) = @_; + return "ignored: missing repo/branch\n" unless $repo && $branch; + + # Fire and forget - deploy.pl daemonises itself and logs to + # /tmp/lacuna-deploy.log. + system('perl', $deploy_script, $repo, $branch); + return "deploying $repo / $branch\n"; +} + +sub { + my $env = shift; + my $req = Plack::Request->new($env); + + if ($req->method eq 'GET') { + return [ 200, [ 'Content-Type' => 'text/plain' ], + [ "Lacuna deploy webhook. POST a git push payload here.\n" ] ]; + } + + if ($req->method ne 'POST') { + return [ 405, [ 'Content-Type' => 'text/plain' ], [ "method not allowed\n" ] ]; + } + + # Accept either a raw JSON body or the classic form-encoded `payload` param. + my $raw = $req->parameters->{payload} // $req->content // ''; + my $payload = eval { decode_json($raw) } || {}; + + my $repo = $payload->{repository}{name}; + my ($branch) = ($payload->{ref} // '') =~ m{refs/heads/(.+)$}; + + my $msg = eval { dispatch($repo, $branch) } || "error: $@\n"; + return [ 200, [ 'Content-Type' => 'text/plain' ], [ $msg ] ]; +}; diff --git a/bin/lacuna.logrotate b/bin/lacuna.logrotate index 95a97278..fcd66551 120000 --- a/bin/lacuna.logrotate +++ b/bin/lacuna.logrotate @@ -1 +1,51 @@ -../../Lacuna-Server-Private/bin/lacuna.logrotate \ No newline at end of file +# logrotate configuration for The Lacuna Expanse server. +# +# Reconstructed: this file only ever existed in this repo as a symlink into the +# (now gone) Lacuna-Server-Private repo. It is consumed by the OS logrotate cron, +# not by any repo code. Install it as /etc/logrotate.d/lacuna (or point logrotate +# at it directly). +# +# The server's cron drivers (bin/run_*.sh), the schedule daemons +# (bin/schedule_{building,ship_arrival,captcha}.pl) and the deploy webhook all +# append to flat files under /tmp; the plack/starman processes write to +# /home/lacuna/server/log. Everything is rotated with copytruncate because those +# long-running processes keep the log file handles open and have no reopen signal. + +/tmp/hourly.log +/tmp/run_two_hourly.log +/tmp/four_hourly.log +/tmp/daily.log +/tmp/weekly.log +/tmp/weekly_medals.log +/tmp/news_feeds.log +/tmp/tick_planets.log +/tmp/tick_spies.log +/tmp/tick_fissures.log +/tmp/test_weather.log +/tmp/schedule_building.log +/tmp/schedule_ship_arrival.log +/tmp/schedule_captcha.log +/tmp/attack_saben.log +/tmp/attack_diab.log +/tmp/attack_trel.log +/tmp/lacuna-deploy.log +{ + daily + rotate 7 + missingok + notifempty + compress + delaycompress + copytruncate +} + +/home/lacuna/server/log/*.log { + daily + rotate 14 + missingok + notifempty + compress + delaycompress + copytruncate + sharedscripts +} diff --git a/bin/startqa.sh b/bin/startqa.sh index 1ac14bd4..dedd3c80 100755 --- a/bin/startqa.sh +++ b/bin/startqa.sh @@ -1,2 +1,22 @@ -echo "This script can now be found at /data/Lacuna-Server-Private/bin/startqa.sh" +#!/bin/bash +# +# Start the Lacuna server on the QA box. +# +# This script previously lived in the (now gone) Lacuna-Server-Private repo; +# bin/deploy.pl execs it after pulling a new revision. Reconstructed from +# bin/start_pt.sh, which is the equivalent launcher for the "pt" environment. +export PATH=/data/apps/bin:$PATH +cd /home/lacuna/server/bin + +perl generate_docs.pl > /dev/null + +memcached -d -u nobody -m 512 + +fuser -k 5000/tcp +fuser -k 5001/tcp + +start_server --port 5000 -- starman --user nobody --group nobody --workers 3 --preload-app lacuna.psgi & +start_server --port 5001 -- starman --workers 1 --user nobody --group nobody --preload-app deploy.psgi & + +service nginx start diff --git a/lib/Lacuna/AI/Cult.pm b/lib/Lacuna/AI/Cult.pm index 825254d9..49ae08b4 100644 --- a/lib/Lacuna/AI/Cult.pm +++ b/lib/Lacuna/AI/Cult.pm @@ -81,6 +81,14 @@ sub colony_structures { ); } +sub extra_glyph_buildings { + return { + quantity => 0, + min_level => 1, + max_level => 5, + }; +} + sub spy_missions { # Missions run by script return ( diff --git a/lib/Lacuna/AI/DeLambert.pm b/lib/Lacuna/AI/DeLambert.pm index f482e745..eedd1e7d 100644 --- a/lib/Lacuna/AI/DeLambert.pm +++ b/lib/Lacuna/AI/DeLambert.pm @@ -12,6 +12,135 @@ extends 'Lacuna::AI'; use constant empire_id => -9; +# --------------------------------------------------------------------------- +# Colony / empire generation. +# +# These four members were removed from this module in 2011 (commit 3bd1635d) +# when bulk colony seeding was moved into the standalone bin/delambert/add_colonies.pl. +# They were never replaced here, so Lacuna::AI's lazy `empire` builder (which +# calls create_empire when empire -9 does not yet exist) had nothing to call and +# bin/delambert/hourly_update.pl died with "Can't locate object method ...". +# +# Restored (modelled on the sibling AIs and on add_colonies.pl's create_empire / +# create_colony) so the DeLambert AI can bootstrap itself from cron exactly like +# Saben/Trelvestian/Diablotin/Jackpot do. add_colonies.pl is still the tool for +# seeding a whole galaxy of trading posts; this just covers the founding case. +# --------------------------------------------------------------------------- + +has viable_colonies => ( + is => 'ro', + lazy => 1, + default => sub { + return Lacuna->db->resultset('Lacuna::DB::Result::Map::Body')->search( + { + empire_id => undef, + class => { -like => ['%Planet::GasGiant::G%', '%::Planet::P%'] }, + orbit => { '<' => 8 }, + size => { '>=' => 50 }, + }, + ); + } +); + +sub empire_defaults { + return { + name => 'DeLambert', + status_message => 'We Come in Peace', + description => 'A peaceful trading empire.', + species_name => 'DeLamberti', + species_description => 'A strong species who prefer high gravity worlds.', + min_orbit => 1, + max_orbit => 7, + manufacturing_affinity => 2, + deception_affinity => 7, + research_affinity => 2, + management_affinity => 7, + farming_affinity => 1, + mining_affinity => 1, + science_affinity => 7, + environmental_affinity => 2, + political_affinity => 1, + trade_affinity => 7, + growth_affinity => 1, + is_isolationist => 0, + }; +} + +sub colony_structures { + return ( + ['Lacuna::DB::Result::Building::Permanent::EssentiaVein', 1], + ['Lacuna::DB::Result::Building::Permanent::TheDillonForge', 25], + ['Lacuna::DB::Result::Building::Permanent::GeoThermalVent', 25], + ['Lacuna::DB::Result::Building::Permanent::NaturalSpring', 25], + ['Lacuna::DB::Result::Building::Permanent::Volcano', 24], + ['Lacuna::DB::Result::Building::Permanent::AlgaePond', 25], + ['Lacuna::DB::Result::Building::Permanent::MalcudField', 25], + ['Lacuna::DB::Result::Building::Trade', 25], + ['Lacuna::DB::Result::Building::Transporter', 25], + ['Lacuna::DB::Result::Building::Intelligence', 25], + ['Lacuna::DB::Result::Building::Security', 25], + ['Lacuna::DB::Result::Building::Espionage', 23], + ['Lacuna::DB::Result::Building::MunitionsLab', 20], + ['Lacuna::DB::Result::Building::Archaeology', 20], + ['Lacuna::DB::Result::Building::Observatory', 15], + ['Lacuna::DB::Result::Building::Shipyard', 26], + ['Lacuna::DB::Result::Building::Shipyard', 26], + ['Lacuna::DB::Result::Building::Shipyard', 26], + ['Lacuna::DB::Result::Building::Shipyard', 26], + ['Lacuna::DB::Result::Building::SpacePort', 25], + ['Lacuna::DB::Result::Building::SpacePort', 25], + ['Lacuna::DB::Result::Building::SpacePort', 25], + ['Lacuna::DB::Result::Building::SpacePort', 25], + ['Lacuna::DB::Result::Building::SpacePort', 25], + ['Lacuna::DB::Result::Building::Energy::Reserve', 25], + ['Lacuna::DB::Result::Building::Food::Reserve', 25], + ['Lacuna::DB::Result::Building::Ore::Storage', 25], + ['Lacuna::DB::Result::Building::Water::Storage', 25], + ['Lacuna::DB::Result::Building::Food::Malcud', 20], + ['Lacuna::DB::Result::Building::Food::Algae', 20], + ['Lacuna::DB::Result::Building::Ore::Refinery', 15], + ['Lacuna::DB::Result::Building::Ore::Mine', 20], + ['Lacuna::DB::Result::Building::Ore::Mine', 20], + ['Lacuna::DB::Result::Building::Energy::Singularity', 20], + ['Lacuna::DB::Result::Building::Energy::Singularity', 20], + ['Lacuna::DB::Result::Building::Water::Production', 20], + ['Lacuna::DB::Result::Building::Water::AtmosphericEvaporator', 20], + ['Lacuna::DB::Result::Building::Waste::Sequestration', 20], + ['Lacuna::DB::Result::Building::Waste::Exchanger', 15], + ['Lacuna::DB::Result::Building::SAW', 15], + ['Lacuna::DB::Result::Building::SAW', 15], + ['Lacuna::DB::Result::Building::SAW', 15], + ['Lacuna::DB::Result::Building::SAW', 15], + ['Lacuna::DB::Result::Building::SAW', 15], + ['Lacuna::DB::Result::Building::SAW', 15], + ); +} + +sub extra_glyph_buildings { + return { + quantity => 0, + min_level => 1, + max_level => 5, + }; +} + +# The base `scratch` attribute just does a search() and can hand back undef. +# DeLambert's hourly code leans on scratch->pad->{status} everywhere, so make +# sure the empire-wide scratch pad always exists (mirrors add_colonies.pl). +has '+scratch' => ( + default => sub { + my $self = shift; + my $rs = Lacuna->db->resultset('Lacuna::DB::Result::AIScratchPad'); + my ($scratch) = $rs->search({ ai_empire_id => $self->empire_id, body_id => 0 }); + $scratch ||= $rs->create({ + ai_empire_id => $self->empire_id, + body_id => 0, + pad => { status => 'peace' }, + }); + return $scratch; + }, +); + sub spy_missions { return ( 'Appropriate Resources', @@ -21,11 +150,15 @@ sub spy_missions { sub ship_building_priorities { my ($self, $colony) = @_; - my $status = $self->scratch->pad->{status}; + my $status = $self->scratch->pad->{status} || 'peace'; print " Status is [$status]\n"; my $scratch = $self->get_colony_scratchpad($colony); my $level = $scratch->pad->{level}; + # snap to one of the quota bands (5,10,..30) - a colony founded by the base + # AI bootstrap has no explicit level in its pad + my ($band) = grep { $_ <= ($level || 0) } (30, 25, 20, 15, 10, 5); + $level = $band || 5; my $quota = { peace => { @@ -128,11 +261,23 @@ sub run_hourly_empire_updates { sub get_colony_scratchpad { my ($self, $colony) = @_; - my ($scratch) = Lacuna::db->resultset('Lacuna::DB::Result::AIScratchPad')->search({ + my $rs = Lacuna::db->resultset('Lacuna::DB::Result::AIScratchPad'); + my ($scratch) = $rs->search({ ai_empire_id => $self->empire_id, body_id => $colony->id, }); + # A colony founded by the base AI bootstrap (rather than add_colonies.pl) + # has no per-colony pad yet; seed one from the PCC level. + if (not $scratch) { + my $pcc_level = eval { $colony->command->level } || 5; + $scratch = $rs->create({ + ai_empire_id => $self->empire_id, + body_id => $colony->id, + pad => { level => $pcc_level }, + }); + } + return $scratch; } @@ -441,7 +586,9 @@ sub retaliate { my $empire = $self->empire; $self->scratch->discard_changes; my $scratch_pad = $self->scratch->pad; - my $attack = $scratch_pad->{attack}; + # {attack} is only ever populated by the admin war-status UI; treat "unset" + # as "nothing to retaliate for" rather than dereferencing undef. + my $attack = $scratch_pad->{attack} || {}; my @del_colonies = Lacuna->db->resultset('Lacuna::DB::Result::Map::Body')->search({ empire_id => -9, diff --git a/lib/Lacuna/DB/Result/AIBattleSummary.pm b/lib/Lacuna/DB/Result/AIBattleSummary.pm index 897f1a5e..6c12e8de 100644 --- a/lib/Lacuna/DB/Result/AIBattleSummary.pm +++ b/lib/Lacuna/DB/Result/AIBattleSummary.pm @@ -9,16 +9,17 @@ use DateTime; __PACKAGE__->table('ai_battle_summary'); __PACKAGE__->add_columns( - id => { data_type => 'int', size => 11, is_nullable => 0 }, + id => { data_type => 'int', size => 11, is_nullable => 0, is_auto_increment => 1 }, attacking_empire_id => { data_type => 'int', size => 11, is_nullable => 0 }, defending_empire_id => { data_type => 'int', size => 11, is_nullable => 0 }, - attack_victories => { data_type => 'int', size => 11, is_nullable => 0 }, - defense_victories => { data_type => 'int', size => 11, is_nullable => 0 }, - attack_spy_hours => { data_type => 'int', size => 11, is_nullable => 0 }, -); + attack_victories => { data_type => 'int', size => 11, is_nullable => 0, default_value => 0 }, + defense_victories => { data_type => 'int', size => 11, is_nullable => 0, default_value => 0 }, + attack_spy_hours => { data_type => 'int', size => 11, is_nullable => 0, default_value => 0 }, +); +__PACKAGE__->set_primary_key('id'); -__PACKAGE__->belongs_to('empire', 'Lacuna::DB::Result::Empire', 'attacking_empire_id'); -__PACKAGE__->belongs_to('empire', 'Lacuna::DB::Result::Empire', 'defending_empire_id'); +__PACKAGE__->belongs_to('attacker', 'Lacuna::DB::Result::Empire', 'attacking_empire_id'); +__PACKAGE__->belongs_to('defender', 'Lacuna::DB::Result::Empire', 'defending_empire_id'); no Moose; __PACKAGE__->meta->make_immutable(inline_constructor => 0); diff --git a/lib/Lacuna/DB/Result/AIScratchPad.pm b/lib/Lacuna/DB/Result/AIScratchPad.pm index 8ba8127c..d8a0aae9 100644 --- a/lib/Lacuna/DB/Result/AIScratchPad.pm +++ b/lib/Lacuna/DB/Result/AIScratchPad.pm @@ -9,12 +9,16 @@ use DateTime; __PACKAGE__->table('ai_scratch_pad'); __PACKAGE__->add_columns( - id => { data_type => 'int', size => 11, is_nullable => 0 }, + id => { data_type => 'int', size => 11, is_nullable => 0, is_auto_increment => 1 }, ai_empire_id => { data_type => 'int', size => 11, is_nullable => 0 }, body_id => { data_type => 'int', size => 11, is_nullable => 1 }, pad => { data_type => 'mediumblob', is_nullable => 1, 'serializer_class' => 'JSON' }, -); +); +__PACKAGE__->set_primary_key('id'); +# body_id == 0 is the "whole empire" scratch pad sentinel (see Lacuna::AI::scratch +# and bin//add_colonies.pl), so this relation is only meaningful for the +# per-colony pads. __PACKAGE__->belongs_to('body', 'Lacuna::DB::Result::Map::Body', 'body_id'); __PACKAGE__->belongs_to('empire', 'Lacuna::DB::Result::Empire', 'ai_empire_id'); diff --git a/lib/Lacuna/DB/Result/Log/Battles.pm b/lib/Lacuna/DB/Result/Log/Battles.pm index 8ee37525..f3cd0a7b 100644 --- a/lib/Lacuna/DB/Result/Log/Battles.pm +++ b/lib/Lacuna/DB/Result/Log/Battles.pm @@ -45,18 +45,25 @@ after 'sqlt_deploy_hook' => sub { after insert => sub { my $self = shift; - # only store summary information for attacking or defending AI - return if ($self->attacking_empire_id > 1 and (not defined $self->defending_empire_id or $self->defending_empire_id > 1 )); + # Attacker-stat rows carry an empty-string defending_empire_id (see + # TriggerDefense::log_attack) and have no real opponent to summarise against; + # only the paired defender-stat rows (with a concrete defending_empire_id) + # feed ai_battle_summary. + my $def = $self->defending_empire_id; + return unless (defined $def and $def ne '' and $def =~ /^-?\d+$/); + + # only store summary information when an AI empire is on one side + return if ($self->attacking_empire_id > 1 and $def > 1); my $summary_rs = Lacuna->db->resultset('Lacuna::DB::Result::AIBattleSummary'); my $ai_battle_summary = $summary_rs->search({ attacking_empire_id => $self->attacking_empire_id, - defending_empire_id => $self->defending_empire_id, + defending_empire_id => $def, })->first; if (not $ai_battle_summary) { $ai_battle_summary = $summary_rs->create({ attacking_empire_id => $self->attacking_empire_id, - defending_empire_id => $self->defending_empire_id, + defending_empire_id => $def, attack_victories => 0, defense_victories => 0, }); diff --git a/lib/Lacuna/DB/Result/Ships.pm b/lib/Lacuna/DB/Result/Ships.pm index 0f208458..9003505f 100644 --- a/lib/Lacuna/DB/Result/Ships.pm +++ b/lib/Lacuna/DB/Result/Ships.pm @@ -216,11 +216,20 @@ sub arrive { return; } elsif ($reason) { - # this is unexpected, so let's rethrow - confess $reason; + # Something in the arrival handling blew up unexpectedly. The Schedule + # row (and its beanstalk job) is already gone, so re-throwing here would + # strand the ship in 'Travelling' forever with nothing left to retry it. + # Log it loudly and still resolve the journey (land / turn around) so the + # ship is recoverable. + my $detail = ref $reason eq 'ARRAY' ? join(' ', map { defined $_ ? $_ : '' } @$reason) : "$reason"; + warn sprintf("Ship %s (%s) arrival handling failed, landing anyway: %s", + $self->id, $self->type, $detail); } - - # no exceptions, so we either need to go home or land + + # If a role deleted the ship before dying, there's nothing left to resolve. + return unless $self->in_storage; + + # no fatal exception, so we either need to go home or land if ($self->direction eq 'out') { $self->turn_around->update; } diff --git a/lib/Lacuna/Role/Ship/Arrive/TriggerDefense.pm b/lib/Lacuna/Role/Ship/Arrive/TriggerDefense.pm index 27e4a7cd..c38566b5 100644 --- a/lib/Lacuna/Role/Ship/Arrive/TriggerDefense.pm +++ b/lib/Lacuna/Role/Ship/Arrive/TriggerDefense.pm @@ -202,7 +202,7 @@ sub log_attack { $new_hash->{attacking_unit_name} = $atype, $new_hash->{attacking_type} = $atype, $new_hash->{attacking_number} = $attack_stat->{$abid}->{$atype}->{number}, - $new_hash->{defending_empire_id} = "", + $new_hash->{defending_empire_id} = undef, $new_hash->{defending_empire_name} = "", $new_hash->{defending_body_id} = 0, $new_hash->{defending_body_name} = "", diff --git a/var/upgrades/4.0002.sql b/var/upgrades/4.0002.sql new file mode 100644 index 00000000..80ba34bf --- /dev/null +++ b/var/upgrades/4.0002.sql @@ -0,0 +1,25 @@ +-- Two AI bookkeeping tables shipped from Result classes that were missing +-- `is_auto_increment => 1` on their `id` column and never called set_primary_key, +-- so DBIx::Class inserted id=0 for the first row and every later insert died with +-- "Duplicate entry '0' for key 'PRIMARY'". For ai_battle_summary that killed the +-- battle-log summary hook, which killed the ship-arrival job, leaving any ship +-- that triggered combat on arrival stuck in task='Travelling' forever. For +-- ai_scratch_pad it blocked the DeLambert AI from ever bootstrapping. +-- +-- The Result classes are now fixed; this brings existing databases into line. +-- (The PRIMARY KEY on `id` already exists on deployed schemas.) + +DELETE FROM ai_battle_summary WHERE id = 0; +ALTER TABLE ai_battle_summary MODIFY COLUMN id int(11) NOT NULL AUTO_INCREMENT; + +DELETE FROM ai_scratch_pad WHERE id = 0; +ALTER TABLE ai_scratch_pad MODIFY COLUMN id int(11) NOT NULL AUTO_INCREMENT; + +-- The whole codebase (Lacuna::AI::scratch, bin//add_colonies.pl, +-- Lacuna::Web::Admin) uses body_id = 0 as the "whole empire" scratch-pad +-- sentinel, but body id 0 does not exist, so this foreign key can never be +-- satisfied for the empire-wide pad. Drop it. (Named ai_scratch_pad_fk_body_id +-- in the shipped schema; if your database does not have it, this line is a +-- harmless no-op error.) The plain KEY ai_scratch_pad_idx_body_id is left in +-- place for lookup performance. +ALTER TABLE ai_scratch_pad DROP FOREIGN KEY ai_scratch_pad_fk_body_id; -- 2.51.2