diff --git a/client.so b/client.so new file mode 100644 index 0000000..cb43ef8 Binary files /dev/null and b/client.so differ diff --git a/client/go.mod b/client/go.mod index a4fa64f..8eff2e5 100644 --- a/client/go.mod +++ b/client/go.mod @@ -4,10 +4,13 @@ go 1.26.0 require ( fyne.io/fyne/v2 v2.7.3 + galaxy/loader v0.0.0 github.com/fogleman/gg v1.3.0 github.com/stretchr/testify v1.11.1 ) +replace galaxy/loader v0.0.0 => ../loader/ + require ( fyne.io/systray v1.12.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect @@ -28,11 +31,10 @@ require ( github.com/hack-pad/safejs v0.1.1 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rymdport/portal v0.4.2 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect @@ -41,6 +43,5 @@ require ( golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/client/go.sum b/client/go.sum index e8b9a6b..0f202ed 100644 --- a/client/go.sum +++ b/client/go.sum @@ -47,23 +47,18 @@ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wH github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= diff --git a/client/plugin/plugin.go b/client/plugin/plugin.go new file mode 100644 index 0000000..bfe4f1c --- /dev/null +++ b/client/plugin/plugin.go @@ -0,0 +1,8 @@ +package main + +import ( + "galaxy/client" + "galaxy/loader" +) + +var Factory loader.ClientInit = client.NewClient diff --git a/server/cmd/http/main.go b/game/cmd/http/main.go similarity index 82% rename from server/cmd/http/main.go rename to game/cmd/http/main.go index 9380fe7..0445b8c 100644 --- a/server/cmd/http/main.go +++ b/game/cmd/http/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "galaxy/server/internal/router" + "galaxy/game/internal/router" ) func main() { diff --git a/server/go.mod b/game/go.mod similarity index 98% rename from server/go.mod rename to game/go.mod index 7c06152..c4b882e 100644 --- a/server/go.mod +++ b/game/go.mod @@ -1,4 +1,4 @@ -module galaxy/server +module galaxy/game go 1.26.0 diff --git a/server/go.sum b/game/go.sum similarity index 100% rename from server/go.sum rename to game/go.sum diff --git a/server/internal/bitmap/assets_test/circle_case_00.txt b/game/internal/bitmap/assets_test/circle_case_00.txt similarity index 100% rename from server/internal/bitmap/assets_test/circle_case_00.txt rename to game/internal/bitmap/assets_test/circle_case_00.txt diff --git a/server/internal/bitmap/assets_test/circle_case_01.txt b/game/internal/bitmap/assets_test/circle_case_01.txt similarity index 100% rename from server/internal/bitmap/assets_test/circle_case_01.txt rename to game/internal/bitmap/assets_test/circle_case_01.txt diff --git a/server/internal/bitmap/assets_test/circle_case_02.txt b/game/internal/bitmap/assets_test/circle_case_02.txt similarity index 100% rename from server/internal/bitmap/assets_test/circle_case_02.txt rename to game/internal/bitmap/assets_test/circle_case_02.txt diff --git a/server/internal/bitmap/assets_test/circle_case_03.txt b/game/internal/bitmap/assets_test/circle_case_03.txt similarity index 100% rename from server/internal/bitmap/assets_test/circle_case_03.txt rename to game/internal/bitmap/assets_test/circle_case_03.txt diff --git a/server/internal/bitmap/assets_test/circle_case_04.txt b/game/internal/bitmap/assets_test/circle_case_04.txt similarity index 100% rename from server/internal/bitmap/assets_test/circle_case_04.txt rename to game/internal/bitmap/assets_test/circle_case_04.txt diff --git a/server/internal/bitmap/bitmap.go b/game/internal/bitmap/bitmap.go similarity index 100% rename from server/internal/bitmap/bitmap.go rename to game/internal/bitmap/bitmap.go diff --git a/server/internal/bitmap/bitmap_test.go b/game/internal/bitmap/bitmap_test.go similarity index 99% rename from server/internal/bitmap/bitmap_test.go rename to game/internal/bitmap/bitmap_test.go index 27c225c..5bcbf94 100644 --- a/server/internal/bitmap/bitmap_test.go +++ b/game/internal/bitmap/bitmap_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "galaxy/server/internal/bitmap" + "galaxy/game/internal/bitmap" ) func TestBitVectorSize(t *testing.T) { diff --git a/server/internal/bitmap/export_test.go b/game/internal/bitmap/export_test.go similarity index 100% rename from server/internal/bitmap/export_test.go rename to game/internal/bitmap/export_test.go diff --git a/server/internal/controller/battle.go b/game/internal/controller/battle.go similarity index 99% rename from server/internal/controller/battle.go rename to game/internal/controller/battle.go index 5292b8a..784ce0a 100644 --- a/server/internal/controller/battle.go +++ b/game/internal/controller/battle.go @@ -7,7 +7,7 @@ import ( "math/rand/v2" "slices" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/battle_test.go b/game/internal/controller/battle_test.go similarity index 98% rename from server/internal/controller/battle_test.go rename to game/internal/controller/battle_test.go index 2d5c434..391f6d0 100644 --- a/server/internal/controller/battle_test.go +++ b/game/internal/controller/battle_test.go @@ -5,8 +5,8 @@ import ( "slices" "testing" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/controller/battle_transform.go b/game/internal/controller/battle_transform.go similarity index 100% rename from server/internal/controller/battle_transform.go rename to game/internal/controller/battle_transform.go diff --git a/server/internal/controller/bombing.go b/game/internal/controller/bombing.go similarity index 98% rename from server/internal/controller/bombing.go rename to game/internal/controller/bombing.go index ad7aa32..85376ca 100644 --- a/server/internal/controller/bombing.go +++ b/game/internal/controller/bombing.go @@ -1,7 +1,7 @@ package controller import ( - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/bombing_test.go b/game/internal/controller/bombing_test.go similarity index 98% rename from server/internal/controller/bombing_test.go rename to game/internal/controller/bombing_test.go index bbfe519..a0f9e86 100644 --- a/server/internal/controller/bombing_test.go +++ b/game/internal/controller/bombing_test.go @@ -3,8 +3,8 @@ package controller_test import ( "testing" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/cache.go b/game/internal/controller/cache.go similarity index 98% rename from server/internal/controller/cache.go rename to game/internal/controller/cache.go index 8bdad97..ce9dab6 100644 --- a/server/internal/controller/cache.go +++ b/game/internal/controller/cache.go @@ -4,7 +4,7 @@ import ( "fmt" "slices" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/command.go b/game/internal/controller/command.go similarity index 99% rename from server/internal/controller/command.go rename to game/internal/controller/command.go index b4ab027..6ba8808 100644 --- a/server/internal/controller/command.go +++ b/game/internal/controller/command.go @@ -5,7 +5,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/controller.go b/game/internal/controller/controller.go similarity index 98% rename from server/internal/controller/controller.go rename to game/internal/controller/controller.go index 988b26f..77173a3 100644 --- a/server/internal/controller/controller.go +++ b/game/internal/controller/controller.go @@ -3,14 +3,14 @@ package controller import ( "errors" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" "galaxy/model/order" "galaxy/model/report" - "galaxy/server/internal/repo" + "galaxy/game/internal/repo" ) type Configurer func(*Param) diff --git a/server/internal/controller/controller_export_test.go b/game/internal/controller/controller_export_test.go similarity index 98% rename from server/internal/controller/controller_export_test.go rename to game/internal/controller/controller_export_test.go index 40fcca0..452afdc 100644 --- a/server/internal/controller/controller_export_test.go +++ b/game/internal/controller/controller_export_test.go @@ -5,7 +5,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/controller_test.go b/game/internal/controller/controller_test.go similarity index 98% rename from server/internal/controller/controller_test.go rename to game/internal/controller/controller_test.go index b0b2946..fadae18 100644 --- a/server/internal/controller/controller_test.go +++ b/game/internal/controller/controller_test.go @@ -3,8 +3,8 @@ package controller_test import ( "fmt" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/fleet.go b/game/internal/controller/fleet.go similarity index 99% rename from server/internal/controller/fleet.go rename to game/internal/controller/fleet.go index 08c0d63..8538dea 100644 --- a/server/internal/controller/fleet.go +++ b/game/internal/controller/fleet.go @@ -10,7 +10,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/fleet_send.go b/game/internal/controller/fleet_send.go similarity index 97% rename from server/internal/controller/fleet_send.go rename to game/internal/controller/fleet_send.go index 49dc512..82827ed 100644 --- a/server/internal/controller/fleet_send.go +++ b/game/internal/controller/fleet_send.go @@ -5,7 +5,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" ) func (c *Cache) FleetSend(ri, fi int, planetNumber uint) error { diff --git a/server/internal/controller/fleet_send_test.go b/game/internal/controller/fleet_send_test.go similarity index 98% rename from server/internal/controller/fleet_send_test.go rename to game/internal/controller/fleet_send_test.go index 6c8b3d2..a02cb64 100644 --- a/server/internal/controller/fleet_send_test.go +++ b/game/internal/controller/fleet_send_test.go @@ -6,7 +6,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/controller/fleet_test.go b/game/internal/controller/fleet_test.go similarity index 99% rename from server/internal/controller/fleet_test.go rename to game/internal/controller/fleet_test.go index d12e468..a5df3aa 100644 --- a/server/internal/controller/fleet_test.go +++ b/game/internal/controller/fleet_test.go @@ -7,7 +7,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/generate_game.go b/game/internal/controller/generate_game.go similarity index 98% rename from server/internal/controller/generate_game.go rename to game/internal/controller/generate_game.go index 96cde46..8c6fbde 100644 --- a/server/internal/controller/generate_game.go +++ b/game/internal/controller/generate_game.go @@ -5,8 +5,8 @@ import ( "math/rand/v2" "slices" - "galaxy/server/internal/generator" - "galaxy/server/internal/model/game" + "galaxy/game/internal/generator" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/generate_game_test.go b/game/internal/controller/generate_game_test.go similarity index 93% rename from server/internal/controller/generate_game_test.go rename to game/internal/controller/generate_game_test.go index 258631b..aded8d0 100644 --- a/server/internal/controller/generate_game_test.go +++ b/game/internal/controller/generate_game_test.go @@ -8,9 +8,9 @@ import ( "galaxy/util" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" - "galaxy/server/internal/repo" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" + "galaxy/game/internal/repo" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/generate_turn.go b/game/internal/controller/generate_turn.go similarity index 99% rename from server/internal/controller/generate_turn.go rename to game/internal/controller/generate_turn.go index 42a4702..34460ee 100644 --- a/server/internal/controller/generate_turn.go +++ b/game/internal/controller/generate_turn.go @@ -6,7 +6,7 @@ import ( "galaxy/model/report" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/order.go b/game/internal/controller/order.go similarity index 99% rename from server/internal/controller/order.go rename to game/internal/controller/order.go index 53a12a7..9759337 100644 --- a/server/internal/controller/order.go +++ b/game/internal/controller/order.go @@ -8,7 +8,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/planet.go b/game/internal/controller/planet.go similarity index 99% rename from server/internal/controller/planet.go rename to game/internal/controller/planet.go index 21bbb35..fade376 100644 --- a/server/internal/controller/planet.go +++ b/game/internal/controller/planet.go @@ -9,7 +9,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/planet_test.go b/game/internal/controller/planet_test.go similarity index 99% rename from server/internal/controller/planet_test.go rename to game/internal/controller/planet_test.go index dea09f5..141e152 100644 --- a/server/internal/controller/planet_test.go +++ b/game/internal/controller/planet_test.go @@ -8,8 +8,8 @@ import ( e "galaxy/error" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/race.go b/game/internal/controller/race.go similarity index 99% rename from server/internal/controller/race.go rename to game/internal/controller/race.go index aaa906a..2c0a551 100644 --- a/server/internal/controller/race.go +++ b/game/internal/controller/race.go @@ -7,7 +7,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" ) func (c *Cache) Relation(r1, r2 int) game.Relation { diff --git a/server/internal/controller/race_test.go b/game/internal/controller/race_test.go similarity index 98% rename from server/internal/controller/race_test.go rename to game/internal/controller/race_test.go index 7411954..388588b 100644 --- a/server/internal/controller/race_test.go +++ b/game/internal/controller/race_test.go @@ -5,7 +5,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/controller/report.go b/game/internal/controller/report.go similarity index 99% rename from server/internal/controller/report.go rename to game/internal/controller/report.go index 75e462c..bf982e5 100644 --- a/server/internal/controller/report.go +++ b/game/internal/controller/report.go @@ -10,7 +10,7 @@ import ( "galaxy/util" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/report_test.go b/game/internal/controller/report_test.go similarity index 100% rename from server/internal/controller/report_test.go rename to game/internal/controller/report_test.go diff --git a/server/internal/controller/route.go b/game/internal/controller/route.go similarity index 99% rename from server/internal/controller/route.go rename to game/internal/controller/route.go index 474f9c2..1b2e11d 100644 --- a/server/internal/controller/route.go +++ b/game/internal/controller/route.go @@ -12,7 +12,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" ) func (c *Cache) PlanetRouteSet(ri int, rt game.RouteType, origin, destination uint) error { diff --git a/server/internal/controller/route_test.go b/game/internal/controller/route_test.go similarity index 99% rename from server/internal/controller/route_test.go rename to game/internal/controller/route_test.go index dc40b20..6adf9d8 100644 --- a/server/internal/controller/route_test.go +++ b/game/internal/controller/route_test.go @@ -6,8 +6,8 @@ import ( e "galaxy/error" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/controller/science.go b/game/internal/controller/science.go similarity index 98% rename from server/internal/controller/science.go rename to game/internal/controller/science.go index b97adca..a33d20e 100644 --- a/server/internal/controller/science.go +++ b/game/internal/controller/science.go @@ -8,7 +8,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/science_test.go b/game/internal/controller/science_test.go similarity index 98% rename from server/internal/controller/science_test.go rename to game/internal/controller/science_test.go index eb00b63..bf88619 100644 --- a/server/internal/controller/science_test.go +++ b/game/internal/controller/science_test.go @@ -5,8 +5,8 @@ import ( e "galaxy/error" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/ship_class.go b/game/internal/controller/ship_class.go similarity index 99% rename from server/internal/controller/ship_class.go rename to game/internal/controller/ship_class.go index c01dafd..961cb16 100644 --- a/server/internal/controller/ship_class.go +++ b/game/internal/controller/ship_class.go @@ -9,7 +9,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/ship_class_test.go b/game/internal/controller/ship_class_test.go similarity index 100% rename from server/internal/controller/ship_class_test.go rename to game/internal/controller/ship_class_test.go diff --git a/server/internal/controller/ship_group.go b/game/internal/controller/ship_group.go similarity index 99% rename from server/internal/controller/ship_group.go rename to game/internal/controller/ship_group.go index 1c1e6ee..66e1722 100644 --- a/server/internal/controller/ship_group.go +++ b/game/internal/controller/ship_group.go @@ -11,7 +11,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/ship_group_move.go b/game/internal/controller/ship_group_move.go similarity index 97% rename from server/internal/controller/ship_group_move.go rename to game/internal/controller/ship_group_move.go index d91ceb7..7d21279 100644 --- a/server/internal/controller/ship_group_move.go +++ b/game/internal/controller/ship_group_move.go @@ -6,7 +6,7 @@ import ( "galaxy/util" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" ) func (c *Cache) MoveShipGroups() { diff --git a/server/internal/controller/ship_group_move_test.go b/game/internal/controller/ship_group_move_test.go similarity index 97% rename from server/internal/controller/ship_group_move_test.go rename to game/internal/controller/ship_group_move_test.go index 0a6b597..4e4bacf 100644 --- a/server/internal/controller/ship_group_move_test.go +++ b/game/internal/controller/ship_group_move_test.go @@ -4,7 +4,7 @@ import ( "slices" "testing" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/controller/ship_group_send.go b/game/internal/controller/ship_group_send.go similarity index 98% rename from server/internal/controller/ship_group_send.go rename to game/internal/controller/ship_group_send.go index 8224faa..a8e362a 100644 --- a/server/internal/controller/ship_group_send.go +++ b/game/internal/controller/ship_group_send.go @@ -5,7 +5,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/ship_group_send_test.go b/game/internal/controller/ship_group_send_test.go similarity index 98% rename from server/internal/controller/ship_group_send_test.go rename to game/internal/controller/ship_group_send_test.go index 6d46896..70275ba 100644 --- a/server/internal/controller/ship_group_send_test.go +++ b/game/internal/controller/ship_group_send_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/uuid" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/controller/ship_group_test.go b/game/internal/controller/ship_group_test.go similarity index 99% rename from server/internal/controller/ship_group_test.go rename to game/internal/controller/ship_group_test.go index df69694..44ecef7 100644 --- a/server/internal/controller/ship_group_test.go +++ b/game/internal/controller/ship_group_test.go @@ -10,7 +10,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/ship_group_upgrade.go b/game/internal/controller/ship_group_upgrade.go similarity index 99% rename from server/internal/controller/ship_group_upgrade.go rename to game/internal/controller/ship_group_upgrade.go index a2e4e10..3dda566 100644 --- a/server/internal/controller/ship_group_upgrade.go +++ b/game/internal/controller/ship_group_upgrade.go @@ -7,7 +7,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/ship_group_upgrade_test.go b/game/internal/controller/ship_group_upgrade_test.go similarity index 98% rename from server/internal/controller/ship_group_upgrade_test.go rename to game/internal/controller/ship_group_upgrade_test.go index 58279e6..393abdf 100644 --- a/server/internal/controller/ship_group_upgrade_test.go +++ b/game/internal/controller/ship_group_upgrade_test.go @@ -5,9 +5,9 @@ import ( e "galaxy/error" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" - g "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" + g "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/controller/vote.go b/game/internal/controller/vote.go similarity index 99% rename from server/internal/controller/vote.go rename to game/internal/controller/vote.go index a76620d..aec6ebb 100644 --- a/server/internal/controller/vote.go +++ b/game/internal/controller/vote.go @@ -7,7 +7,7 @@ import ( "math/big" "slices" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/controller/vote_test.go b/game/internal/controller/vote_test.go similarity index 98% rename from server/internal/controller/vote_test.go rename to game/internal/controller/vote_test.go index d6feae3..dab4e93 100644 --- a/server/internal/controller/vote_test.go +++ b/game/internal/controller/vote_test.go @@ -3,8 +3,8 @@ package controller_test import ( "testing" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/generator/generator.go b/game/internal/generator/generator.go similarity index 100% rename from server/internal/generator/generator.go rename to game/internal/generator/generator.go diff --git a/server/internal/generator/generator_test.go b/game/internal/generator/generator_test.go similarity index 99% rename from server/internal/generator/generator_test.go rename to game/internal/generator/generator_test.go index 8a3fcdc..56641f9 100644 --- a/server/internal/generator/generator_test.go +++ b/game/internal/generator/generator_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "galaxy/server/internal/generator" + "galaxy/game/internal/generator" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/generator/map.go b/game/internal/generator/map.go similarity index 97% rename from server/internal/generator/map.go rename to game/internal/generator/map.go index 257553b..661c4a8 100644 --- a/server/internal/generator/map.go +++ b/game/internal/generator/map.go @@ -6,7 +6,7 @@ import ( "galaxy/util" - "galaxy/server/internal/generator/plotter" + "galaxy/game/internal/generator/plotter" ) type Map struct { diff --git a/server/internal/generator/map_test.go b/game/internal/generator/map_test.go similarity index 100% rename from server/internal/generator/map_test.go rename to game/internal/generator/map_test.go diff --git a/server/internal/generator/planet.go b/game/internal/generator/planet.go similarity index 100% rename from server/internal/generator/planet.go rename to game/internal/generator/planet.go diff --git a/server/internal/generator/planet_test.go b/game/internal/generator/planet_test.go similarity index 95% rename from server/internal/generator/planet_test.go rename to game/internal/generator/planet_test.go index 0bfe300..6995b38 100644 --- a/server/internal/generator/planet_test.go +++ b/game/internal/generator/planet_test.go @@ -4,7 +4,7 @@ import ( "regexp" "testing" - g "galaxy/server/internal/generator" + g "galaxy/game/internal/generator" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/generator/plotter/plotter.go b/game/internal/generator/plotter/plotter.go similarity index 98% rename from server/internal/generator/plotter/plotter.go rename to game/internal/generator/plotter/plotter.go index 98fa68c..fdef37b 100644 --- a/server/internal/generator/plotter/plotter.go +++ b/game/internal/generator/plotter/plotter.go @@ -6,7 +6,7 @@ import ( "math" "math/rand" - "galaxy/server/internal/bitmap" + "galaxy/game/internal/bitmap" ) type Plotter struct { diff --git a/server/internal/generator/plotter/plotter_test.go b/game/internal/generator/plotter/plotter_test.go similarity index 97% rename from server/internal/generator/plotter/plotter_test.go rename to game/internal/generator/plotter/plotter_test.go index 17dc7e6..a4aa9f7 100644 --- a/server/internal/generator/plotter/plotter_test.go +++ b/game/internal/generator/plotter/plotter_test.go @@ -3,7 +3,7 @@ package plotter_test import ( "testing" - "galaxy/server/internal/generator/plotter" + "galaxy/game/internal/generator/plotter" ) func TestNewPlotter(t *testing.T) { diff --git a/server/internal/generator/settings.go b/game/internal/generator/settings.go similarity index 100% rename from server/internal/generator/settings.go rename to game/internal/generator/settings.go diff --git a/server/internal/model/game/bombing.go b/game/internal/model/game/bombing.go similarity index 100% rename from server/internal/model/game/bombing.go rename to game/internal/model/game/bombing.go diff --git a/server/internal/model/game/fleet.go b/game/internal/model/game/fleet.go similarity index 100% rename from server/internal/model/game/fleet.go rename to game/internal/model/game/fleet.go diff --git a/server/internal/model/game/game.go b/game/internal/model/game/game.go similarity index 100% rename from server/internal/model/game/game.go rename to game/internal/model/game/game.go diff --git a/server/internal/model/game/game_test.go b/game/internal/model/game/game_test.go similarity index 98% rename from server/internal/model/game/game_test.go rename to game/internal/model/game/game_test.go index 09c7a7b..4a250b3 100644 --- a/server/internal/model/game/game_test.go +++ b/game/internal/model/game/game_test.go @@ -3,7 +3,7 @@ package game_test import ( "testing" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/model/game/group.go b/game/internal/model/game/group.go similarity index 100% rename from server/internal/model/game/group.go rename to game/internal/model/game/group.go diff --git a/server/internal/model/game/group_test.go b/game/internal/model/game/group_test.go similarity index 99% rename from server/internal/model/game/group_test.go rename to game/internal/model/game/group_test.go index f47408e..7cc8219 100644 --- a/server/internal/model/game/group_test.go +++ b/game/internal/model/game/group_test.go @@ -6,7 +6,7 @@ import ( "galaxy/util" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/model/game/map.go b/game/internal/model/game/map.go similarity index 100% rename from server/internal/model/game/map.go rename to game/internal/model/game/map.go diff --git a/server/internal/model/game/planet.go b/game/internal/model/game/planet.go similarity index 100% rename from server/internal/model/game/planet.go rename to game/internal/model/game/planet.go diff --git a/server/internal/model/game/planet_test.go b/game/internal/model/game/planet_test.go similarity index 98% rename from server/internal/model/game/planet_test.go rename to game/internal/model/game/planet_test.go index f2cd497..5efdf9e 100644 --- a/server/internal/model/game/planet_test.go +++ b/game/internal/model/game/planet_test.go @@ -3,7 +3,7 @@ package game_test import ( "testing" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/model/game/production.go b/game/internal/model/game/production.go similarity index 100% rename from server/internal/model/game/production.go rename to game/internal/model/game/production.go diff --git a/server/internal/model/game/race.go b/game/internal/model/game/race.go similarity index 100% rename from server/internal/model/game/race.go rename to game/internal/model/game/race.go diff --git a/server/internal/model/game/race_test.go b/game/internal/model/game/race_test.go similarity index 94% rename from server/internal/model/game/race_test.go rename to game/internal/model/game/race_test.go index e920b25..014a4c7 100644 --- a/server/internal/model/game/race_test.go +++ b/game/internal/model/game/race_test.go @@ -3,7 +3,7 @@ package game_test import ( "testing" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/model/game/route.go b/game/internal/model/game/route.go similarity index 100% rename from server/internal/model/game/route.go rename to game/internal/model/game/route.go diff --git a/server/internal/model/game/science.go b/game/internal/model/game/science.go similarity index 100% rename from server/internal/model/game/science.go rename to game/internal/model/game/science.go diff --git a/server/internal/model/game/ship.go b/game/internal/model/game/ship.go similarity index 100% rename from server/internal/model/game/ship.go rename to game/internal/model/game/ship.go diff --git a/server/internal/model/game/ship_test.go b/game/internal/model/game/ship_test.go similarity index 94% rename from server/internal/model/game/ship_test.go rename to game/internal/model/game/ship_test.go index 319be2c..4d3420e 100644 --- a/server/internal/model/game/ship_test.go +++ b/game/internal/model/game/ship_test.go @@ -3,7 +3,7 @@ package game_test import ( "testing" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/stretchr/testify/assert" ) diff --git a/server/internal/model/game/state.go b/game/internal/model/game/state.go similarity index 100% rename from server/internal/model/game/state.go rename to game/internal/model/game/state.go diff --git a/server/internal/repo/fs/fs.go b/game/internal/repo/fs/fs.go similarity index 100% rename from server/internal/repo/fs/fs.go rename to game/internal/repo/fs/fs.go diff --git a/server/internal/repo/fs/fs_test.go b/game/internal/repo/fs/fs_test.go similarity index 99% rename from server/internal/repo/fs/fs_test.go rename to game/internal/repo/fs/fs_test.go index a57f0e9..85ab6a8 100644 --- a/server/internal/repo/fs/fs_test.go +++ b/game/internal/repo/fs/fs_test.go @@ -6,7 +6,7 @@ import ( "slices" "testing" - "galaxy/server/internal/repo/fs" + "galaxy/game/internal/repo/fs" "galaxy/util" "github.com/stretchr/testify/assert" diff --git a/server/internal/repo/game.go b/game/internal/repo/game.go similarity index 99% rename from server/internal/repo/game.go rename to game/internal/repo/game.go index d5dc341..26ab0db 100644 --- a/server/internal/repo/game.go +++ b/game/internal/repo/game.go @@ -18,7 +18,7 @@ import ( "galaxy/model/order" "galaxy/model/report" - "galaxy/server/internal/model/game" + "galaxy/game/internal/model/game" "github.com/google/uuid" ) diff --git a/server/internal/repo/repo.go b/game/internal/repo/repo.go similarity index 97% rename from server/internal/repo/repo.go rename to game/internal/repo/repo.go index a81068d..592dce5 100644 --- a/server/internal/repo/repo.go +++ b/game/internal/repo/repo.go @@ -6,7 +6,7 @@ import ( e "galaxy/error" - "galaxy/server/internal/repo/fs" + "galaxy/game/internal/repo/fs" ) func NewStorageError(err error) error { diff --git a/server/internal/repo/repo_export_test.go b/game/internal/repo/repo_export_test.go similarity index 100% rename from server/internal/repo/repo_export_test.go rename to game/internal/repo/repo_export_test.go diff --git a/server/internal/repo/repo_test.go b/game/internal/repo/repo_test.go similarity index 98% rename from server/internal/repo/repo_test.go rename to game/internal/repo/repo_test.go index 7fe672d..5be1c3c 100644 --- a/server/internal/repo/repo_test.go +++ b/game/internal/repo/repo_test.go @@ -6,8 +6,8 @@ import ( "galaxy/model/order" - "galaxy/server/internal/repo" - "galaxy/server/internal/repo/fs" + "galaxy/game/internal/repo" + "galaxy/game/internal/repo/fs" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/router/command_test.go b/game/internal/router/command_test.go similarity index 100% rename from server/internal/router/command_test.go rename to game/internal/router/command_test.go diff --git a/server/internal/router/handler/command.go b/game/internal/router/handler/command.go similarity index 99% rename from server/internal/router/handler/command.go rename to game/internal/router/handler/command.go index 0190b17..a102e5b 100644 --- a/server/internal/router/handler/command.go +++ b/game/internal/router/handler/command.go @@ -6,7 +6,7 @@ import ( "fmt" "net/http" - "galaxy/server/internal/controller" + "galaxy/game/internal/controller" "github.com/go-playground/validator/v10" "github.com/google/uuid" diff --git a/server/internal/router/handler/handler.go b/game/internal/router/handler/handler.go similarity index 97% rename from server/internal/router/handler/handler.go rename to game/internal/router/handler/handler.go index b7a9493..1f5cfb2 100644 --- a/server/internal/router/handler/handler.go +++ b/game/internal/router/handler/handler.go @@ -10,8 +10,8 @@ import ( e "galaxy/error" - "galaxy/server/internal/controller" - "galaxy/server/internal/model/game" + "galaxy/game/internal/controller" + "galaxy/game/internal/model/game" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/server/internal/router/handler/init.go b/game/internal/router/handler/init.go similarity index 100% rename from server/internal/router/handler/init.go rename to game/internal/router/handler/init.go diff --git a/server/internal/router/handler/order.go b/game/internal/router/handler/order.go similarity index 95% rename from server/internal/router/handler/order.go rename to game/internal/router/handler/order.go index dea1e8e..39764fa 100644 --- a/server/internal/router/handler/order.go +++ b/game/internal/router/handler/order.go @@ -7,7 +7,7 @@ import ( "galaxy/model/order" "galaxy/model/rest" - "galaxy/server/internal/repo" + "galaxy/game/internal/repo" "github.com/gin-gonic/gin" ) diff --git a/server/internal/router/handler/status.go b/game/internal/router/handler/status.go similarity index 100% rename from server/internal/router/handler/status.go rename to game/internal/router/handler/status.go diff --git a/server/internal/router/handler/turn.go b/game/internal/router/handler/turn.go similarity index 100% rename from server/internal/router/handler/turn.go rename to game/internal/router/handler/turn.go diff --git a/server/internal/router/init_test.go b/game/internal/router/init_test.go similarity index 90% rename from server/internal/router/init_test.go rename to game/internal/router/init_test.go index 5b2acaa..0431295 100644 --- a/server/internal/router/init_test.go +++ b/game/internal/router/init_test.go @@ -10,9 +10,9 @@ import ( "galaxy/util" - "galaxy/server/internal/controller" - "galaxy/server/internal/router" - "galaxy/server/internal/router/handler" + "galaxy/game/internal/controller" + "galaxy/game/internal/router" + "galaxy/game/internal/router/handler" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/router/middleware.go b/game/internal/router/middleware.go similarity index 100% rename from server/internal/router/middleware.go rename to game/internal/router/middleware.go diff --git a/server/internal/router/order_test.go b/game/internal/router/order_test.go similarity index 100% rename from server/internal/router/order_test.go rename to game/internal/router/order_test.go diff --git a/server/internal/router/router.go b/game/internal/router/router.go similarity index 98% rename from server/internal/router/router.go rename to game/internal/router/router.go index 2e5441c..ca7d246 100644 --- a/server/internal/router/router.go +++ b/game/internal/router/router.go @@ -6,7 +6,7 @@ import ( "net/http" "os" - "galaxy/server/internal/router/handler" + "galaxy/game/internal/router/handler" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" diff --git a/server/internal/router/router_export_test.go b/game/internal/router/router_export_test.go similarity index 80% rename from server/internal/router/router_export_test.go rename to game/internal/router/router_export_test.go index 31d4fa6..1041bb7 100644 --- a/server/internal/router/router_export_test.go +++ b/game/internal/router/router_export_test.go @@ -1,7 +1,7 @@ package router import ( - "galaxy/server/internal/router/handler" + "galaxy/game/internal/router/handler" "github.com/gin-gonic/gin" ) diff --git a/server/internal/router/router_helper_test.go b/game/internal/router/router_helper_test.go similarity index 95% rename from server/internal/router/router_helper_test.go rename to game/internal/router/router_helper_test.go index e2aef3a..804e5b9 100644 --- a/server/internal/router/router_helper_test.go +++ b/game/internal/router/router_helper_test.go @@ -7,8 +7,8 @@ import ( "galaxy/model/order" "galaxy/model/rest" - "galaxy/server/internal/router" - "galaxy/server/internal/router/handler" + "galaxy/game/internal/router" + "galaxy/game/internal/router/handler" "github.com/gin-gonic/gin" "github.com/google/uuid" diff --git a/server/internal/router/router_test.go b/game/internal/router/router_test.go similarity index 98% rename from server/internal/router/router_test.go rename to game/internal/router/router_test.go index 1fe050e..d0c63b5 100644 --- a/server/internal/router/router_test.go +++ b/game/internal/router/router_test.go @@ -12,7 +12,7 @@ import ( "galaxy/model/rest" - "galaxy/server/internal/router" + "galaxy/game/internal/router" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" diff --git a/server/internal/router/status_test.go b/game/internal/router/status_test.go similarity index 93% rename from server/internal/router/status_test.go rename to game/internal/router/status_test.go index 4de4fc6..c5ea005 100644 --- a/server/internal/router/status_test.go +++ b/game/internal/router/status_test.go @@ -10,9 +10,9 @@ import ( "galaxy/util" - "galaxy/server/internal/controller" - "galaxy/server/internal/router" - "galaxy/server/internal/router/handler" + "galaxy/game/internal/controller" + "galaxy/game/internal/router" + "galaxy/game/internal/router/handler" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/router/turn_test.go b/game/internal/router/turn_test.go similarity index 94% rename from server/internal/router/turn_test.go rename to game/internal/router/turn_test.go index 6d7bb23..e5b7784 100644 --- a/server/internal/router/turn_test.go +++ b/game/internal/router/turn_test.go @@ -10,9 +10,9 @@ import ( "galaxy/util" - "galaxy/server/internal/controller" - "galaxy/server/internal/router" - "galaxy/server/internal/router/handler" + "galaxy/game/internal/controller" + "galaxy/game/internal/router" + "galaxy/game/internal/router/handler" "github.com/google/uuid" "github.com/stretchr/testify/assert" diff --git a/server/internal/router/validator.go b/game/internal/router/validator.go similarity index 100% rename from server/internal/router/validator.go rename to game/internal/router/validator.go diff --git a/go.work b/go.work index 5303702..1c1fdd7 100644 --- a/go.work +++ b/go.work @@ -2,16 +2,17 @@ go 1.26.0 use ( ./client - ./connector + ./game ./loader + ./pkg/connector ./pkg/error ./pkg/model ./pkg/storage ./pkg/util - ./server ) replace ( + galaxy/connector v0.0.0 => ./pkg/connector galaxy/error v0.0.0 => ./pkg/error galaxy/model v0.0.0 => ./pkg/model galaxy/storage v0.0.0 => ./pkg/storage diff --git a/go.work.sum b/go.work.sum index 465be66..b0b6e05 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2,25 +2,26 @@ github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxk github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jackmordaunt/icns/v2 v2.2.6/go.mod h1:DqlVnR5iafSphrId7aSD06r3jg0KRC9V6lEBBp504ZQ= github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lucor/goinfo v0.9.0/go.mod h1:L6m6tN5Rlova5Z83h1ZaKsMP1iiaoZ9vGTNzu5QKOD4= github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -39,5 +40,4 @@ golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/loader/cmd/main.go b/loader/cmd/main.go index 027c12a..c4f6680 100644 --- a/loader/cmd/main.go +++ b/loader/cmd/main.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "galaxy/loader" + "galaxy/storage/fs" "os" "os/signal" @@ -28,8 +29,12 @@ func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - app := app.New() - l, err := loader.NewLoader(ctx, app, nil) + app := app.NewWithID("galaxy-client") + s, err := fs.NewFS(app.Storage().RootURI().Path()) + if err != nil { + return + } + l, err := loader.NewLoader(ctx, s, nil, app) if err != nil { return } diff --git a/loader/loader.go b/loader/loader.go index 14fbc4c..31db0c4 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -16,9 +16,9 @@ import ( type ClientInit func(context.Context, storage.UIStorage, connector.UIConnector, fyne.App) (mc.Client, error) type loader struct { - conn connector.Connector - cli mc.Client - storagePath string + s storage.Storage + conn connector.Connector + cli mc.Client } const ( @@ -30,27 +30,25 @@ var ( checkVersionTimeout = time.Minute * 60 ) -func NewLoader(ctx context.Context, app fyne.App, conn connector.Connector) (*loader, error) { - storagePath, err := initStorage(app) - if err != nil { - return nil, err - } - var s storage.Storage = nil - cli, err := loadClientPlugin(ctx, s, conn, app, "./client.so", "NewClient") +func NewLoader(ctx context.Context, s storage.Storage, conn connector.Connector, app fyne.App) (*loader, error) { + cli, err := loadClientPlugin(ctx, s, conn, app, "./client.so", "Factory") if err != nil { return nil, err } l := &loader{ - conn: conn, - cli: cli, - storagePath: storagePath, + conn: conn, + cli: cli, + s: s, } return l, nil } func (l *loader) Run(ctx context.Context) error { final := make(chan struct{}, 1) - go l.backgroundLoop(ctx, final) + if l.conn != nil { + go l.backgroundLoop(ctx, final) + defer func() { final <- struct{}{} }() + } if err := l.cli.Run(); err != nil { return err } diff --git a/loader/util.go b/loader/util.go index 63823b1..cd76bbe 100644 --- a/loader/util.go +++ b/loader/util.go @@ -4,8 +4,6 @@ import ( "galaxy/connector" "runtime" "slices" - - "fyne.io/fyne/v2" ) func resolvePluginFile(version string) string { @@ -25,9 +23,3 @@ func latestVersion(versions []connector.VersionInfo) (connector.VersionInfo, boo slices.SortFunc(versions, func(a, b connector.VersionInfo) int { return compareSemver(b.Version, a.Version) }) return versions[0], true } - -// initStorage returns filesystem storage root or error if initialization fails. -func initStorage(app fyne.App) (string, error) { - _ = app.Storage() // use fyne.App's Storage - return "", nil -} diff --git a/connector/connector.go b/pkg/connector/connector.go similarity index 100% rename from connector/connector.go rename to pkg/connector/connector.go diff --git a/connector/go.mod b/pkg/connector/go.mod similarity index 100% rename from connector/go.mod rename to pkg/connector/go.mod diff --git a/connector/http/http.go b/pkg/connector/http/http.go similarity index 100% rename from connector/http/http.go rename to pkg/connector/http/http.go diff --git a/connector/http/http_test.go b/pkg/connector/http/http_test.go similarity index 100% rename from connector/http/http_test.go rename to pkg/connector/http/http_test.go diff --git a/pkg/storage/fs/fs.go b/pkg/storage/fs/fs.go index eda3467..e608f9c 100644 --- a/pkg/storage/fs/fs.go +++ b/pkg/storage/fs/fs.go @@ -1,75 +1,704 @@ -// fs implements galaxy/storage.Storage with filesystem +// Package fs implements galaxy/storage.Storage using the filesystem. package fs -/* - -Общие правила: - -1. Все хранимые объекты сериализуются / десериализуются как JSON. - -2. Структура хранения файлов: - -- storageRoot \ - | - +-- state.dat - | - +-- {GameID} \ - | | - | +-- {Turn}.dat (client.GameData) - | +-- {Turn}.dat (client.GameData) - | +-- ... - | - +-- {GameID} \ - | - +-- ... - -*/ - import ( + "encoding/json" + "errors" "fmt" - "galaxy/util" + "os" "path/filepath" + "slices" + "strconv" + "strings" + "sync" + + "galaxy/model/client" + "galaxy/model/order" + "galaxy/model/report" + "galaxy/util" ) const ( - // Name of the file under the storage's root where [model.State] is stored. + // stateFileName is the file name under the storage root where [client.State] is stored. stateFileName = "state.dat" - // Suffix of a Game's file inder the storage's root where [model.GameData] is stored. + // gameDataFileSuffix is the extension for per-turn [client.GameData] files. gameDataFileSuffix = ".dat" + + defaultFilePerm = 0o644 + oldFileSuffix = ".old" + newFileSuffix = ".new" ) -// StateFilePath returns client's state file path relative to the root, -// file name and extension are pre-defined constant. +// StateFilePath returns the path to the persisted [client.State] file under root. func StateFilePath(root string) string { return filepath.Join(root, stateFileName) } -// GameDataPath returns game's data file path relative to the root, -// data file name is GameID string representation and extension is a pre-defined constant. +// GameDataFilePath returns the legacy per-game data file path under root. +// +// The storage implementation keeps turn data in per-turn files under a game directory +// and does not use this helper internally. func GameDataFilePath(root string, id fmt.Stringer) string { return filepath.Join(root, id.String()) + gameDataFileSuffix } -type fsStorage struct { - storageRoot string +type pathLock struct { + mu sync.Mutex + refs int } -// NewFS returns on-filesystem implementation of the "galaxy/storage.Storage" with root located at storageRoot. -// storageRoot must me a directory and has write access to the current user. If initial checks failed, return nil and non-nil error. -func NewStorage(storageRoot string) (*fsStorage, error) { - if ok, err := util.PathExists(storageRoot, true); err != nil { - return nil, fmt.Errorf("new storage: check path %q exists: %w", storageRoot, err) - } else if !ok { - return nil, fmt.Errorf("new storage: path %q does not exists", storageRoot) - } - if ok, err := util.Writable(storageRoot); err != nil { - return nil, fmt.Errorf("new storage: check path %q writable: %w", storageRoot, err) - } else if !ok { - return nil, fmt.Errorf("new storage: path %q is not writable", storageRoot) - } - s := &fsStorage{ - storageRoot: storageRoot, - } - return s, nil +type fsStorage struct { + storageRoot string + + locksMu sync.Mutex + locks map[string]*pathLock + + readFileFn func(string) ([]byte, error) + writeFileFn func(string, []byte, os.FileMode) error + renameFileFn func(string, string) error + removeFileFn func(string) error +} + +type storedOrder struct { + UpdatedAt int `json:"updatedAt"` + Commands []json.RawMessage `json:"cmd"` +} + +type storedGameData struct { + Turn uint `json:"turn"` + Report report.Report `json:"report"` + Order *storedOrder `json:"order,omitempty"` +} + +// NewFS returns a filesystem-backed implementation of galaxy/storage.Storage rooted at storageRoot. +// storageRoot must already exist, be a directory, and be writable by the current user. +func NewFS(storageRoot string) (*fsStorage, error) { + fmt.Println("using fs root:", storageRoot) + absRoot, err := filepath.Abs(storageRoot) + if err != nil { + return nil, fmt.Errorf("new fs storage: resolve absolute path for %q: %w", storageRoot, err) + } + if ok, err := util.PathExists(absRoot, true); err != nil { + return nil, fmt.Errorf("new fs storage: check path %q exists: %w", absRoot, err) + } else if !ok { + return nil, fmt.Errorf("new fs storage: path %q does not exist", absRoot) + } + if ok, err := util.Writable(absRoot); err != nil { + return nil, fmt.Errorf("new fs storage: check path %q writable: %w", absRoot, err) + } else if !ok { + return nil, fmt.Errorf("new fs storage: path %q is not writable", absRoot) + } + + return &fsStorage{ + storageRoot: absRoot, + locks: make(map[string]*pathLock), + readFileFn: os.ReadFile, + writeFileFn: os.WriteFile, + renameFileFn: os.Rename, + removeFileFn: os.Remove, + }, nil +} + +func (s *fsStorage) StateExists(callback func(bool, error)) { + go func() { + exists, err := s.FileExists(stateFileName) + if callback != nil { + callback(exists, err) + } + }() +} + +func (s *fsStorage) LoadState(callback func(client.State, error)) { + go func() { + state, err := s.loadStateSync() + if callback != nil { + callback(state, err) + } + }() +} + +func (s *fsStorage) SaveState(state client.State, callback func(error)) { + go func() { + err := s.saveStateSync(state) + if callback != nil { + callback(err) + } + }() +} + +func (s *fsStorage) LoadReport(id client.GameID, turn uint, callback func(report.Report, error)) { + go func() { + rep, err := s.loadReportSync(id, turn) + if callback != nil { + callback(rep, err) + } + }() +} + +func (s *fsStorage) SaveReport(id client.GameID, turn uint, rep report.Report, callback func(error)) { + go func() { + err := s.saveReportSync(id, turn, rep) + if callback != nil { + callback(err) + } + }() +} + +func (s *fsStorage) LoadOrder(id client.GameID, turn uint, callback func(order.Order, error)) { + go func() { + o, err := s.loadOrderSync(id, turn) + if callback != nil { + callback(o, err) + } + }() +} + +func (s *fsStorage) SaveOrder(id client.GameID, turn uint, o order.Order, callback func(error)) { + go func() { + err := s.saveOrderSync(id, turn, o) + if callback != nil { + callback(err) + } + }() +} + +func (s *fsStorage) FileExists(path string) (bool, error) { + absPath, err := s.resolvePath(path) + if err != nil { + return false, err + } + + var exists bool + err = s.withPathLock(absPath, func() error { + var opErr error + exists, opErr = s.fileExistsUnlocked(absPath) + return opErr + }) + return exists, err +} + +func (s *fsStorage) ReadFile(path string) ([]byte, error) { + absPath, err := s.resolvePath(path) + if err != nil { + return nil, err + } + + var data []byte + err = s.withPathLock(absPath, func() error { + var opErr error + data, opErr = s.readFileUnlocked(absPath) + return opErr + }) + return data, err +} + +func (s *fsStorage) WriteFile(path string, data []byte) error { + absPath, err := s.resolvePath(path) + if err != nil { + return err + } + + return s.withPathLock(absPath, func() error { + return s.writeFileUnlocked(absPath, data) + }) +} + +func (s *fsStorage) DeleteFile(path string) error { + absPath, err := s.resolvePath(path) + if err != nil { + return err + } + + return s.withPathLock(absPath, func() error { + exists, err := s.fileExistsUnlocked(absPath) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("delete file %q: %w", absPath, os.ErrNotExist) + } + if err := s.removeFileFn(absPath); err != nil { + return fmt.Errorf("delete file %q: %w", absPath, err) + } + return nil + }) +} + +func (s *fsStorage) ListFiles() ([]string, error) { + files := make([]string, 0) + err := filepath.WalkDir(s.storageRoot, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + return nil + } + if strings.HasSuffix(d.Name(), newFileSuffix) || strings.HasSuffix(d.Name(), oldFileSuffix) { + return nil + } + + relPath, err := filepath.Rel(s.storageRoot, path) + if err != nil { + return fmt.Errorf("resolve relative path for %q: %w", path, err) + } + files = append(files, filepath.Clean(relPath)) + return nil + }) + if err != nil { + return nil, fmt.Errorf("list files under %q: %w", s.storageRoot, err) + } + + slices.Sort(files) + return files, nil +} + +func (s *fsStorage) loadStateSync() (client.State, error) { + data, err := s.ReadFile(stateFileName) + if err != nil { + return client.State{}, err + } + return unmarshalState(data) +} + +func (s *fsStorage) saveStateSync(state client.State) error { + data, err := marshalState(state) + if err != nil { + return err + } + return s.WriteFile(stateFileName, data) +} + +func (s *fsStorage) loadReportSync(id client.GameID, turn uint) (report.Report, error) { + gameData, err := s.loadGameDataSync(id, turn) + if err != nil { + return report.Report{}, err + } + return gameData.Report, nil +} + +func (s *fsStorage) saveReportSync(id client.GameID, turn uint, rep report.Report) error { + absPath, err := s.resolvePath(gameTurnFilePath(id, turn)) + if err != nil { + return err + } + + return s.withPathLock(absPath, func() error { + gameData, err := s.loadGameDataUnlocked(absPath) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + gameData = client.GameData{Turn: turn} + } + + gameData.Turn = turn + gameData.Report = rep + return s.writeGameDataUnlocked(absPath, gameData) + }) +} + +func (s *fsStorage) loadOrderSync(id client.GameID, turn uint) (order.Order, error) { + gameData, err := s.loadGameDataSync(id, turn) + if err != nil { + return order.Order{}, err + } + if gameData.Order == nil { + return order.Order{}, fmt.Errorf("load order for game %q turn %d: %w", id, turn, os.ErrNotExist) + } + return *gameData.Order, nil +} + +func (s *fsStorage) saveOrderSync(id client.GameID, turn uint, o order.Order) error { + absPath, err := s.resolvePath(gameTurnFilePath(id, turn)) + if err != nil { + return err + } + + return s.withPathLock(absPath, func() error { + gameData, err := s.loadGameDataUnlocked(absPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("save order for game %q turn %d: %w", id, turn, os.ErrNotExist) + } + return err + } + + gameData.Turn = turn + gameData.Order = &o + return s.writeGameDataUnlocked(absPath, gameData) + }) +} + +func (s *fsStorage) loadGameDataSync(id client.GameID, turn uint) (client.GameData, error) { + absPath, err := s.resolvePath(gameTurnFilePath(id, turn)) + if err != nil { + return client.GameData{}, err + } + + var gameData client.GameData + err = s.withPathLock(absPath, func() error { + var opErr error + gameData, opErr = s.loadGameDataUnlocked(absPath) + return opErr + }) + return gameData, err +} + +func (s *fsStorage) loadGameDataUnlocked(absPath string) (client.GameData, error) { + data, err := s.readFileUnlocked(absPath) + if err != nil { + return client.GameData{}, err + } + return unmarshalGameData(data) +} + +func (s *fsStorage) writeGameDataUnlocked(absPath string, gameData client.GameData) error { + data, err := marshalGameData(gameData) + if err != nil { + return err + } + return s.writeFileUnlocked(absPath, data) +} + +func marshalState(state client.State) ([]byte, error) { + return marshalJSON(state) +} + +func unmarshalState(data []byte) (client.State, error) { + var state client.State + if err := unmarshalJSON(data, &state); err != nil { + return client.State{}, err + } + return state, nil +} + +func marshalGameData(gameData client.GameData) ([]byte, error) { + stored, err := makeStoredGameData(gameData) + if err != nil { + return nil, err + } + return marshalJSON(stored) +} + +func unmarshalGameData(data []byte) (client.GameData, error) { + var stored storedGameData + if err := unmarshalJSON(data, &stored); err != nil { + return client.GameData{}, err + } + return stored.toGameData() +} + +func marshalJSON(value any) ([]byte, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("marshal json: %w", err) + } + return data, nil +} + +func unmarshalJSON(data []byte, target any) error { + if err := json.Unmarshal(data, target); err != nil { + return fmt.Errorf("unmarshal json: %w", err) + } + return nil +} + +func makeStoredGameData(gameData client.GameData) (storedGameData, error) { + stored := storedGameData{ + Turn: gameData.Turn, + Report: gameData.Report, + } + if gameData.Order == nil { + return stored, nil + } + + storedOrder, err := makeStoredOrder(*gameData.Order) + if err != nil { + return storedGameData{}, err + } + stored.Order = &storedOrder + return stored, nil +} + +func (d storedGameData) toGameData() (client.GameData, error) { + gameData := client.GameData{ + Turn: d.Turn, + Report: d.Report, + } + if d.Order == nil { + return gameData, nil + } + + o, err := d.Order.toOrder() + if err != nil { + return client.GameData{}, err + } + gameData.Order = o + return gameData, nil +} + +func makeStoredOrder(o order.Order) (storedOrder, error) { + result := storedOrder{ + UpdatedAt: o.UpdatedAt, + Commands: make([]json.RawMessage, len(o.Commands)), + } + for i := range o.Commands { + data, err := marshalJSON(o.Commands[i]) + if err != nil { + return storedOrder{}, fmt.Errorf("marshal order command %d: %w", i, err) + } + result.Commands[i] = data + } + return result, nil +} + +func (o *storedOrder) toOrder() (*order.Order, error) { + if o == nil { + return nil, nil + } + + result := &order.Order{ + UpdatedAt: o.UpdatedAt, + Commands: make([]order.DecodableCommand, len(o.Commands)), + } + for i := range o.Commands { + cmd, err := parseOrderCommand(o.Commands[i]) + if err != nil { + return nil, fmt.Errorf("decode order command %d: %w", i, err) + } + result.Commands[i] = cmd + } + return result, nil +} + +func parseOrderCommand(data json.RawMessage) (order.DecodableCommand, error) { + meta := new(order.CommandMeta) + if err := json.Unmarshal(data, meta); err != nil { + return nil, fmt.Errorf("decode order command metadata: %w", err) + } + + switch meta.CmdType { + case order.CommandTypeRaceQuit: + return decodeOrderCommand(data, new(order.CommandRaceQuit)) + case order.CommandTypeRaceVote: + return decodeOrderCommand(data, new(order.CommandRaceVote)) + case order.CommandTypeRaceRelation: + return decodeOrderCommand(data, new(order.CommandRaceRelation)) + case order.CommandTypeShipClassCreate: + return decodeOrderCommand(data, new(order.CommandShipClassCreate)) + case order.CommandTypeShipClassMerge: + return decodeOrderCommand(data, new(order.CommandShipClassMerge)) + case order.CommandTypeShipClassRemove: + return decodeOrderCommand(data, new(order.CommandShipClassRemove)) + case order.CommandTypeShipGroupBreak: + return decodeOrderCommand(data, new(order.CommandShipGroupBreak)) + case order.CommandTypeShipGroupLoad: + return decodeOrderCommand(data, new(order.CommandShipGroupLoad)) + case order.CommandTypeShipGroupUnload: + return decodeOrderCommand(data, new(order.CommandShipGroupUnload)) + case order.CommandTypeShipGroupSend: + return decodeOrderCommand(data, new(order.CommandShipGroupSend)) + case order.CommandTypeShipGroupUpgrade: + return decodeOrderCommand(data, new(order.CommandShipGroupUpgrade)) + case order.CommandTypeShipGroupMerge: + return decodeOrderCommand(data, new(order.CommandShipGroupMerge)) + case order.CommandTypeShipGroupDismantle: + return decodeOrderCommand(data, new(order.CommandShipGroupDismantle)) + case order.CommandTypeShipGroupTransfer: + return decodeOrderCommand(data, new(order.CommandShipGroupTransfer)) + case order.CommandTypeShipGroupJoinFleet: + return decodeOrderCommand(data, new(order.CommandShipGroupJoinFleet)) + case order.CommandTypeFleetMerge: + return decodeOrderCommand(data, new(order.CommandFleetMerge)) + case order.CommandTypeFleetSend: + return decodeOrderCommand(data, new(order.CommandFleetSend)) + case order.CommandTypeScienceCreate: + return decodeOrderCommand(data, new(order.CommandScienceCreate)) + case order.CommandTypeScienceRemove: + return decodeOrderCommand(data, new(order.CommandScienceRemove)) + case order.CommandTypePlanetRename: + return decodeOrderCommand(data, new(order.CommandPlanetRename)) + case order.CommandTypePlanetProduce: + return decodeOrderCommand(data, new(order.CommandPlanetProduce)) + case order.CommandTypePlanetRouteSet: + return decodeOrderCommand(data, new(order.CommandPlanetRouteSet)) + case order.CommandTypePlanetRouteRemove: + return decodeOrderCommand(data, new(order.CommandPlanetRouteRemove)) + default: + return nil, fmt.Errorf("unknown order command type %q", meta.CmdType) + } +} + +func decodeOrderCommand[T order.DecodableCommand](data json.RawMessage, target T) (T, error) { + if err := json.Unmarshal(data, target); err != nil { + return target, err + } + return target, nil +} + +func (s *fsStorage) resolvePath(path string) (string, error) { + relPath, err := normalizeRelativePath(path) + if err != nil { + return "", err + } + return filepath.Join(s.storageRoot, relPath), nil +} + +func normalizeRelativePath(path string) (string, error) { + path = strings.ReplaceAll(path, "\\", string(filepath.Separator)) + path = strings.TrimLeft(path, string(filepath.Separator)) + path = filepath.Clean(path) + + switch { + case path == "." || path == "": + return "", errors.New("path must not be empty") + case filepath.IsAbs(path): + return "", fmt.Errorf("path %q must be relative", path) + case filepath.VolumeName(path) != "": + return "", fmt.Errorf("path %q must not include a volume name", path) + case path == "..": + return "", fmt.Errorf("path %q escapes storage root", path) + case strings.HasPrefix(path, ".."+string(filepath.Separator)): + return "", fmt.Errorf("path %q escapes storage root", path) + } + + return path, nil +} + +func gameTurnFilePath(id fmt.Stringer, turn uint) string { + return filepath.Join(id.String(), strconv.FormatUint(uint64(turn), 10)+gameDataFileSuffix) +} + +func (s *fsStorage) withPathLock(absPath string, fn func() error) error { + lock := s.acquirePathLock(absPath) + defer s.releasePathLock(absPath) + + lock.mu.Lock() + defer lock.mu.Unlock() + + return fn() +} + +func (s *fsStorage) acquirePathLock(absPath string) *pathLock { + s.locksMu.Lock() + defer s.locksMu.Unlock() + + lock, ok := s.locks[absPath] + if !ok { + lock = &pathLock{} + s.locks[absPath] = lock + } + lock.refs++ + return lock +} + +func (s *fsStorage) releasePathLock(absPath string) { + s.locksMu.Lock() + defer s.locksMu.Unlock() + + lock, ok := s.locks[absPath] + if !ok { + return + } + lock.refs-- + if lock.refs == 0 { + delete(s.locks, absPath) + } +} + +func (s *fsStorage) fileExistsUnlocked(absPath string) (bool, error) { + ok, err := util.FileExists(absPath) + if err != nil { + return false, fmt.Errorf("check file %q exists: %w", absPath, err) + } + return ok, nil +} + +func (s *fsStorage) readFileUnlocked(absPath string) ([]byte, error) { + data, err := s.readFileFn(absPath) + if err != nil { + return nil, fmt.Errorf("read file %q: %w", absPath, err) + } + return data, nil +} + +func (s *fsStorage) writeFileUnlocked(absPath string, data []byte) error { + if err := s.ensureParentDir(absPath); err != nil { + return err + } + + targetExists, err := s.fileExistsUnlocked(absPath) + if err != nil { + return err + } + + oldPath := absPath + oldFileSuffix + oldExists, err := s.fileExistsUnlocked(oldPath) + if err != nil { + return err + } + if oldExists { + return fmt.Errorf("write file %q: old file already exists at %q", absPath, oldPath) + } + + newPath := absPath + newFileSuffix + newExists, err := s.fileExistsUnlocked(newPath) + if err != nil { + return err + } + if newExists { + return fmt.Errorf("write file %q: new file already exists at %q", absPath, newPath) + } + + if err := s.writeFileFn(newPath, data, defaultFilePerm); err != nil { + return fmt.Errorf("write new file %q: %w", newPath, err) + } + + if targetExists { + if err := s.renameFileFn(absPath, oldPath); err != nil { + return errors.Join( + fmt.Errorf("rename file %q to %q: %w", absPath, oldPath, err), + s.cleanupTempFile(newPath), + ) + } + } + + if err := s.renameFileFn(newPath, absPath); err != nil { + var restoreErr error + if targetExists { + restoreErr = s.renameFileFn(oldPath, absPath) + if restoreErr != nil { + restoreErr = fmt.Errorf("restore file %q from %q: %w", absPath, oldPath, restoreErr) + } + } + return errors.Join( + fmt.Errorf("rename new file %q to %q: %w", newPath, absPath, err), + restoreErr, + s.cleanupTempFile(newPath), + ) + } + + if !targetExists { + return nil + } + if err := s.removeFileFn(oldPath); err != nil { + return fmt.Errorf("remove old file %q: %w", oldPath, err) + } + return nil +} + +func (s *fsStorage) cleanupTempFile(path string) error { + if err := s.removeFileFn(path); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("remove temp file %q: %w", path, err) + } + return nil +} + +func (s *fsStorage) ensureParentDir(absPath string) error { + parentDir := filepath.Dir(absPath) + if err := os.MkdirAll(parentDir, os.ModePerm); err != nil { + return fmt.Errorf("create parent directory %q: %w", parentDir, err) + } + return nil } diff --git a/pkg/storage/fs/fs_test.go b/pkg/storage/fs/fs_test.go new file mode 100644 index 0000000..b3e82ca --- /dev/null +++ b/pkg/storage/fs/fs_test.go @@ -0,0 +1,529 @@ +package fs + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "sync/atomic" + "testing" + "time" + + "galaxy/model/client" + "galaxy/model/order" + "galaxy/model/report" +) + +const testTimeout = time.Second + +type callbackResult[T any] struct { + value T + err error +} + +func TestStateRoundTripAsync(t *testing.T) { + s := newTestStorage(t) + want := sampleState() + + saveDone := make(chan error, 1) + s.SaveState(want, func(err error) { + saveDone <- err + }) + if err := waitError(t, saveDone); err != nil { + t.Fatalf("save state: %v", err) + } + + existsDone := make(chan callbackResult[bool], 1) + s.StateExists(func(ok bool, err error) { + existsDone <- callbackResult[bool]{value: ok, err: err} + }) + exists := waitResult(t, existsDone) + if exists.err != nil { + t.Fatalf("state exists: %v", exists.err) + } + if !exists.value { + t.Fatal("state file should exist after save") + } + + loadDone := make(chan callbackResult[client.State], 1) + s.LoadState(func(state client.State, err error) { + loadDone <- callbackResult[client.State]{value: state, err: err} + }) + got := waitResult(t, loadDone) + if got.err != nil { + t.Fatalf("load state: %v", got.err) + } + if !reflect.DeepEqual(got.value, want) { + t.Fatalf("loaded state mismatch\nwant: %#v\ngot: %#v", want, got.value) + } +} + +func TestReportAndOrderRoundTripAsync(t *testing.T) { + s := newTestStorage(t) + id := client.GameID("game-1") + turn := uint(7) + initialReport := sampleReport(turn, "Terran") + updatedReport := sampleReport(turn, "Zenith") + wantOrder := sampleOrder() + + saveReportDone := make(chan error, 1) + s.SaveReport(id, turn, initialReport, func(err error) { + saveReportDone <- err + }) + if err := waitError(t, saveReportDone); err != nil { + t.Fatalf("save report: %v", err) + } + + saveOrderDone := make(chan error, 1) + s.SaveOrder(id, turn, wantOrder, func(err error) { + saveOrderDone <- err + }) + if err := waitError(t, saveOrderDone); err != nil { + t.Fatalf("save order: %v", err) + } + + saveUpdatedReportDone := make(chan error, 1) + s.SaveReport(id, turn, updatedReport, func(err error) { + saveUpdatedReportDone <- err + }) + if err := waitError(t, saveUpdatedReportDone); err != nil { + t.Fatalf("save updated report: %v", err) + } + + loadReportDone := make(chan callbackResult[report.Report], 1) + s.LoadReport(id, turn, func(rep report.Report, err error) { + loadReportDone <- callbackResult[report.Report]{value: rep, err: err} + }) + gotReport := waitResult(t, loadReportDone) + if gotReport.err != nil { + t.Fatalf("load report: %v", gotReport.err) + } + if !reflect.DeepEqual(gotReport.value, updatedReport) { + t.Fatalf("loaded report mismatch\nwant: %#v\ngot: %#v", updatedReport, gotReport.value) + } + + loadOrderDone := make(chan callbackResult[order.Order], 1) + s.LoadOrder(id, turn, func(got order.Order, err error) { + loadOrderDone <- callbackResult[order.Order]{value: got, err: err} + }) + gotOrder := waitResult(t, loadOrderDone) + if gotOrder.err != nil { + t.Fatalf("load order: %v", gotOrder.err) + } + if !reflect.DeepEqual(gotOrder.value, wantOrder) { + t.Fatalf("loaded order mismatch\nwant: %#v\ngot: %#v", wantOrder, gotOrder.value) + } +} + +func TestSaveOrderBeforeReportReturnsNotExist(t *testing.T) { + s := newTestStorage(t) + + done := make(chan error, 1) + s.SaveOrder("game-2", 3, sampleOrder(), func(err error) { + done <- err + }) + err := waitError(t, done) + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("save order error = %v, want os.ErrNotExist", err) + } +} + +func TestRawFileCRUDAndList(t *testing.T) { + s := newTestStorage(t) + + if err := s.WriteFile("/nested/alpha.txt", []byte("alpha")); err != nil { + t.Fatalf("write alpha: %v", err) + } + if err := s.WriteFile("beta.txt", []byte("beta")); err != nil { + t.Fatalf("write beta: %v", err) + } + + alphaExists, err := s.FileExists("nested/alpha.txt") + if err != nil { + t.Fatalf("file exists: %v", err) + } + if !alphaExists { + t.Fatal("nested/alpha.txt should exist") + } + + alphaData, err := s.ReadFile("nested/alpha.txt") + if err != nil { + t.Fatalf("read alpha: %v", err) + } + if string(alphaData) != "alpha" { + t.Fatalf("read alpha = %q, want %q", alphaData, "alpha") + } + + if err := os.WriteFile(filepath.Join(s.storageRoot, "skip.txt"+newFileSuffix), []byte("tmp"), 0o644); err != nil { + t.Fatalf("create stale .new file: %v", err) + } + if err := os.WriteFile(filepath.Join(s.storageRoot, "skip.txt"+oldFileSuffix), []byte("tmp"), 0o644); err != nil { + t.Fatalf("create stale .old file: %v", err) + } + + files, err := s.ListFiles() + if err != nil { + t.Fatalf("list files: %v", err) + } + wantFiles := []string{ + "beta.txt", + filepath.Join("nested", "alpha.txt"), + } + if !reflect.DeepEqual(files, wantFiles) { + t.Fatalf("listed files mismatch\nwant: %#v\ngot: %#v", wantFiles, files) + } + + if err := s.DeleteFile("beta.txt"); err != nil { + t.Fatalf("delete beta: %v", err) + } + if err := s.DeleteFile("beta.txt"); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("delete missing beta error = %v, want os.ErrNotExist", err) + } +} + +func TestPathTraversalRejected(t *testing.T) { + s := newTestStorage(t) + + for _, path := range []string{"../escape.txt", "..\\escape.txt", ""} { + t.Run(path, func(t *testing.T) { + err := s.WriteFile(path, []byte("blocked")) + if err == nil { + t.Fatalf("write %q unexpectedly succeeded", path) + } + }) + } +} + +func TestAtomicWriteFirstAndOverwrite(t *testing.T) { + s := newTestStorage(t) + target := filepath.Join("turns", "12.bin") + + if err := s.WriteFile(target, []byte("first")); err != nil { + t.Fatalf("first write: %v", err) + } + assertFileContent(t, s, target, "first") + assertNoTempArtifacts(t, s, target) + + if err := s.WriteFile(target, []byte("second")); err != nil { + t.Fatalf("overwrite: %v", err) + } + assertFileContent(t, s, target, "second") + assertNoTempArtifacts(t, s, target) +} + +func TestAtomicWriteStaleTempCollision(t *testing.T) { + t.Run("stale new file", func(t *testing.T) { + s := newTestStorage(t) + target := "collision-new.txt" + absTarget, err := s.resolvePath(target) + if err != nil { + t.Fatalf("resolve target: %v", err) + } + if err := os.MkdirAll(filepath.Dir(absTarget), os.ModePerm); err != nil { + t.Fatalf("create parent dir: %v", err) + } + if err := os.WriteFile(absTarget+newFileSuffix, []byte("stale"), 0o644); err != nil { + t.Fatalf("write stale new file: %v", err) + } + + err = s.WriteFile(target, []byte("payload")) + if err == nil || !strings.Contains(err.Error(), "new file already exists") { + t.Fatalf("write error = %v, want stale new file error", err) + } + }) + + t.Run("stale old file", func(t *testing.T) { + s := newTestStorage(t) + target := "collision-old.txt" + absTarget, err := s.resolvePath(target) + if err != nil { + t.Fatalf("resolve target: %v", err) + } + if err := os.WriteFile(absTarget, []byte("current"), 0o644); err != nil { + t.Fatalf("write target: %v", err) + } + if err := os.WriteFile(absTarget+oldFileSuffix, []byte("stale"), 0o644); err != nil { + t.Fatalf("write stale old file: %v", err) + } + + err = s.WriteFile(target, []byte("payload")) + if err == nil || !strings.Contains(err.Error(), "old file already exists") { + t.Fatalf("write error = %v, want stale old file error", err) + } + }) +} + +func TestAtomicWriteRollbackOnRenameFailure(t *testing.T) { + s := newTestStorage(t) + target := filepath.Join("rollback", "state.txt") + absTarget, err := s.resolvePath(target) + if err != nil { + t.Fatalf("resolve target: %v", err) + } + + if err := s.WriteFile(target, []byte("original")); err != nil { + t.Fatalf("seed target file: %v", err) + } + + origRename := s.renameFileFn + s.renameFileFn = func(oldPath, newPath string) error { + if oldPath == absTarget+newFileSuffix && newPath == absTarget { + return errors.New("forced rename failure") + } + return origRename(oldPath, newPath) + } + + err = s.WriteFile(target, []byte("replacement")) + if err == nil || !strings.Contains(err.Error(), "forced rename failure") { + t.Fatalf("write error = %v, want forced rename failure", err) + } + + assertFileContent(t, s, target, "original") + assertNoTempArtifacts(t, s, target) +} + +func TestSamePathOperationsSerialize(t *testing.T) { + s := newTestStorage(t) + target := "shared.txt" + absTarget, err := s.resolvePath(target) + if err != nil { + t.Fatalf("resolve target: %v", err) + } + + entered := make(chan struct{}) + release := make(chan struct{}) + origWrite := s.writeFileFn + var writes atomic.Int32 + s.writeFileFn = func(path string, data []byte, perm os.FileMode) error { + if path == absTarget+newFileSuffix && writes.Add(1) == 1 { + close(entered) + <-release + } + return origWrite(path, data, perm) + } + + firstDone := make(chan error, 1) + go func() { + firstDone <- s.WriteFile(target, []byte("one")) + }() + waitSignal(t, entered, "first write entered") + + secondDone := make(chan error, 1) + go func() { + secondDone <- s.WriteFile(target, []byte("two")) + }() + + select { + case err := <-secondDone: + t.Fatalf("second write finished before first released: %v", err) + case <-time.After(50 * time.Millisecond): + } + if writes.Load() != 1 { + t.Fatalf("same-path write reached file hook %d times before release, want 1", writes.Load()) + } + + close(release) + if err := waitError(t, firstDone); err != nil { + t.Fatalf("first write: %v", err) + } + if err := waitError(t, secondDone); err != nil { + t.Fatalf("second write: %v", err) + } +} + +func TestDifferentPathOperationsDoNotBlockEachOther(t *testing.T) { + s := newTestStorage(t) + blockedTarget := "blocked.txt" + absTarget, err := s.resolvePath(blockedTarget) + if err != nil { + t.Fatalf("resolve blocked target: %v", err) + } + + entered := make(chan struct{}) + release := make(chan struct{}) + origWrite := s.writeFileFn + s.writeFileFn = func(path string, data []byte, perm os.FileMode) error { + if path == absTarget+newFileSuffix { + close(entered) + <-release + } + return origWrite(path, data, perm) + } + + blockedDone := make(chan error, 1) + go func() { + blockedDone <- s.WriteFile(blockedTarget, []byte("blocked")) + }() + waitSignal(t, entered, "blocked write entered") + + freeDone := make(chan error, 1) + go func() { + freeDone <- s.WriteFile("free.txt", []byte("free")) + }() + + select { + case err := <-freeDone: + if err != nil { + t.Fatalf("free write: %v", err) + } + case <-time.After(testTimeout): + t.Fatal("write for a different path should not block") + } + + close(release) + if err := waitError(t, blockedDone); err != nil { + t.Fatalf("blocked write: %v", err) + } +} + +func TestSaveStateIsNonBlockingAndCallbackBased(t *testing.T) { + s := newTestStorage(t) + + entered := make(chan struct{}) + release := make(chan struct{}) + origWrite := s.writeFileFn + s.writeFileFn = func(path string, data []byte, perm os.FileMode) error { + close(entered) + <-release + return origWrite(path, data, perm) + } + + callbacks := make(chan error, 2) + s.SaveState(sampleState(), func(err error) { + callbacks <- err + }) + + waitSignal(t, entered, "async save entered") + + select { + case err := <-callbacks: + t.Fatalf("callback fired before storage write completed: %v", err) + default: + } + + close(release) + if err := waitError(t, callbacks); err != nil { + t.Fatalf("callback error: %v", err) + } + + select { + case err := <-callbacks: + t.Fatalf("callback fired more than once: %v", err) + case <-time.After(50 * time.Millisecond): + } +} + +func newTestStorage(t *testing.T) *fsStorage { + t.Helper() + + s, err := NewFS(t.TempDir()) + if err != nil { + t.Fatalf("new test storage: %v", err) + } + return s +} + +func sampleState() client.State { + return client.State{ + GameState: []client.GameState{ + {ID: client.GameID("game-1"), LastTurn: 12, ActiveTurn: 11}, + {ID: client.GameID("game-2"), LastTurn: 4, ActiveTurn: 4}, + }, + ActiveGameID: client.GameID("game-2"), + } +} + +func sampleReport(turn uint, race string) report.Report { + return report.Report{ + Turn: turn, + Width: 160, + Height: 90, + PlanetCount: 8, + Race: race, + VoteFor: "assembly", + } +} + +func sampleOrder() order.Order { + return order.Order{ + UpdatedAt: 1700, + Commands: []order.DecodableCommand{ + &order.CommandPlanetRename{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypePlanetRename, + CmdID: "rename-planet", + }, + Number: 2, + Name: "Nova Prime", + }, + &order.CommandRaceVote{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypeRaceVote, + CmdID: "vote-race", + }, + Acceptor: "ZENITH", + }, + }, + } +} + +func assertFileContent(t *testing.T, s *fsStorage, path, want string) { + t.Helper() + + got, err := s.ReadFile(path) + if err != nil { + t.Fatalf("read %q: %v", path, err) + } + if string(got) != want { + t.Fatalf("content for %q = %q, want %q", path, got, want) + } +} + +func assertNoTempArtifacts(t *testing.T, s *fsStorage, path string) { + t.Helper() + + absPath, err := s.resolvePath(path) + if err != nil { + t.Fatalf("resolve path %q: %v", path, err) + } + for _, tempPath := range []string{absPath + newFileSuffix, absPath + oldFileSuffix} { + if _, err := os.Stat(tempPath); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("temp artifact %q should not exist, stat err = %v", tempPath, err) + } + } +} + +func waitSignal(t *testing.T, ch <-chan struct{}, name string) { + t.Helper() + + select { + case <-ch: + case <-time.After(testTimeout): + t.Fatalf("timeout waiting for %s", name) + } +} + +func waitError(t *testing.T, ch <-chan error) error { + t.Helper() + + select { + case err := <-ch: + return err + case <-time.After(testTimeout): + t.Fatal("timeout waiting for error callback") + return nil + } +} + +func waitResult[T any](t *testing.T, ch <-chan callbackResult[T]) callbackResult[T] { + t.Helper() + + select { + case result := <-ch: + return result + case <-time.After(testTimeout): + t.Fatal("timeout waiting for callback result") + return callbackResult[T]{} + } +} diff --git a/pkg/util/semver.go b/pkg/util/semver.go new file mode 100644 index 0000000..2981de3 --- /dev/null +++ b/pkg/util/semver.go @@ -0,0 +1,120 @@ +package util + +import ( + "cmp" + "fmt" + "strconv" + "strings" +) + +// SemVer stores a numeric semantic version as major, minor, patch, and build +// components. Components that are not provided are represented as zero values. +type SemVer struct { + Major uint + Minor uint + Patch uint + Build uint +} + +// NewSemver constructs a SemVer from v. +// +// NewSemver requires between one and four components ordered as major, minor, +// patch, and build. Components that are not provided are set to zero. +func NewSemver(v ...uint) (SemVer, error) { + s := &SemVer{} + switch len(v) { + case 4: + s.Build = v[3] + fallthrough + case 3: + s.Patch = v[2] + fallthrough + case 2: + s.Minor = v[1] + fallthrough + case 1: + s.Major = v[0] + default: + return *s, fmt.Errorf("new semver: incorrect args count: %d", len(v)) + } + return *s, nil +} + +// MustSemver returns the SemVer produced by NewSemver(v...). +// +// MustSemver panics if NewSemver returns an error. +func MustSemver(v ...uint) SemVer { + if v, err := NewSemver(v...); err != nil { + panic(err) + } else { + return v + } +} + +// ParseSemver parses input into a SemVer. +// +// ParseSemver accepts versions with one to four numeric components separated by +// dots, for example "1", "1.2", "1.2.3", or "1.2.3.4". The input may also use +// the optional "v" or "v." prefix. Missing minor, patch, and build components +// are set to zero. +func ParseSemver(input string) (SemVer, error) { + source := input + + switch { + case strings.HasPrefix(input, "v."): + input = strings.TrimPrefix(input, "v.") + case strings.HasPrefix(input, "v"): + input = strings.TrimPrefix(input, "v") + } + + if input == "" { + return SemVer{}, fmt.Errorf("parse semver %q: missing major version", source) + } + + parts := strings.Split(input, ".") + if len(parts) > 4 { + return SemVer{}, fmt.Errorf("parse semver %q: too many version parts: %d", source, len(parts)) + } + + values := make([]uint, 0, len(parts)) + for idx, part := range parts { + if part == "" { + return SemVer{}, fmt.Errorf("parse semver %q: empty version part at position %d", source, idx+1) + } + + value, err := strconv.ParseUint(part, 10, 0) + if err != nil { + return SemVer{}, fmt.Errorf("parse semver %q: parse part %q at position %d: %w", source, part, idx+1, err) + } + + values = append(values, uint(value)) + } + + version, err := NewSemver(values...) + if err != nil { + return SemVer{}, fmt.Errorf("parse semver %q: %w", source, err) + } + + return version, nil +} + +// ParserSemver calls ParseSemver. +// +// Deprecated: use ParseSemver. +func ParserSemver(input string) (SemVer, error) { + return ParseSemver(input) +} + +// CompareSemver compares two semantic versions and returns: +// +// +1 if x is less than y, +// 0 if x equals y, +// -1 if x is greater than y. +func CompareSemver(x, y SemVer) int { + return cmp.Or( + cmp.Compare(y.Major, x.Major), + cmp.Compare(y.Minor, x.Minor), + cmp.Compare(y.Patch, x.Patch), + cmp.Compare(y.Build, x.Build), + ) +} diff --git a/pkg/util/semver_test.go b/pkg/util/semver_test.go new file mode 100644 index 0000000..460c851 --- /dev/null +++ b/pkg/util/semver_test.go @@ -0,0 +1,280 @@ +package util_test + +import ( + "testing" + + "galaxy/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewSemver(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []uint + want util.SemVer + wantErr string + }{ + { + name: "major only", + input: []uint{1}, + want: util.SemVer{Major: 1}, + }, + { + name: "major and minor", + input: []uint{1, 2}, + want: util.SemVer{Major: 1, Minor: 2}, + }, + { + name: "major minor and patch", + input: []uint{1, 2, 3}, + want: util.SemVer{Major: 1, Minor: 2, Patch: 3}, + }, + { + name: "all components", + input: []uint{1, 2, 3, 4}, + want: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, + }, + { + name: "missing major", + input: nil, + wantErr: "incorrect args count: 0", + }, + { + name: "too many components", + input: []uint{1, 2, 3, 4, 5}, + wantErr: "incorrect args count: 5", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := util.NewSemver(tt.input...) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + assert.Equal(t, util.SemVer{}, got) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestMustSemver(t *testing.T) { + t.Parallel() + + t.Run("returns version", func(t *testing.T) { + t.Parallel() + + assert.Equal(t, util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, util.MustSemver(1, 2, 3, 4)) + }) + + t.Run("panics on invalid input", func(t *testing.T) { + t.Parallel() + + var recovered any + + func() { + defer func() { + recovered = recover() + }() + + util.MustSemver() + }() + + require.NotNil(t, recovered) + + err, ok := recovered.(error) + require.True(t, ok) + assert.EqualError(t, err, "new semver: incorrect args count: 0") + }) +} + +func TestParseSemver(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want util.SemVer + wantErr string + }{ + { + name: "major only", + input: "1", + want: util.SemVer{Major: 1}, + }, + { + name: "major and minor", + input: "1.2", + want: util.SemVer{Major: 1, Minor: 2}, + }, + { + name: "major minor and patch", + input: "1.2.3", + want: util.SemVer{Major: 1, Minor: 2, Patch: 3}, + }, + { + name: "all components", + input: "1.2.3.4", + want: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, + }, + { + name: "v prefix", + input: "v2.3.4.5", + want: util.SemVer{Major: 2, Minor: 3, Patch: 4, Build: 5}, + }, + { + name: "v dot prefix", + input: "v.6.7.8.9", + want: util.SemVer{Major: 6, Minor: 7, Patch: 8, Build: 9}, + }, + { + name: "leading zeros", + input: "v.01.002.0003.0004", + want: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, + }, + { + name: "empty input", + input: "", + wantErr: "missing major version", + }, + { + name: "prefix without version", + input: "v", + wantErr: "missing major version", + }, + { + name: "prefix with dot without version", + input: "v.", + wantErr: "missing major version", + }, + { + name: "leading dot", + input: ".1", + wantErr: "empty version part at position 1", + }, + { + name: "trailing dot", + input: "1.", + wantErr: "empty version part at position 2", + }, + { + name: "empty middle part", + input: "1..2", + wantErr: "empty version part at position 2", + }, + { + name: "too many parts", + input: "1.2.3.4.5", + wantErr: "too many version parts: 5", + }, + { + name: "non numeric part", + input: "1.2.beta", + wantErr: `parse part "beta"`, + }, + { + name: "negative part", + input: "1.-2", + wantErr: `parse part "-2"`, + }, + { + name: "spaces are not accepted", + input: " 1.2 ", + wantErr: `parse part " 1"`, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := util.ParseSemver(tt.input) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + assert.Equal(t, util.SemVer{}, got) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestParserSemver(t *testing.T) { + t.Parallel() + + got, err := util.ParserSemver("v1.2.3.4") + require.NoError(t, err) + assert.Equal(t, util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, got) +} + +func TestCompareSemver(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + x util.SemVer + y util.SemVer + want int + }{ + { + name: "equal versions", + x: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, + y: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, + want: 0, + }, + { + name: "x less by major", + x: util.SemVer{Major: 1, Minor: 9, Patch: 9, Build: 9}, + y: util.SemVer{Major: 2}, + want: 1, + }, + { + name: "x greater by major", + x: util.SemVer{Major: 2}, + y: util.SemVer{Major: 1, Minor: 9, Patch: 9, Build: 9}, + want: -1, + }, + { + name: "x less by minor", + x: util.SemVer{Major: 1, Minor: 1, Patch: 9, Build: 9}, + y: util.SemVer{Major: 1, Minor: 2}, + want: 1, + }, + { + name: "x less by patch", + x: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 9}, + y: util.SemVer{Major: 1, Minor: 2, Patch: 4}, + want: 1, + }, + { + name: "x less by build", + x: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 4}, + y: util.SemVer{Major: 1, Minor: 2, Patch: 3, Build: 5}, + want: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tt.want, util.CompareSemver(tt.x, tt.y)) + }) + } +}