diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09acdb355..a63f0c9d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -279,10 +279,16 @@ jobs: scons -j$(nproc) release=1 server=0 global-gradient-test ./build/src/GlobalGradientHarness + - name: Build and run the map tiling regression + run: | + scons -j$(nproc) release=1 server=0 map-tiling-test + python3 test/run-savegame-safety-tests.py build/src/MapTilingHarness + - name: Build and run the LAN session regression run: | scons -j$(nproc) release=1 server=0 lan-test python3 test/run_lan_session_test.py build/src/LANSessionHarness + python3 test/run_lan_session_test.py build/src/LANSessionHarness --tiled --output output/lan-session-tiled - name: Build and run the aspect-ratio regression env: diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 9d12dd469..42a3b64a0 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2212,3 +2212,17 @@ OpenGL غير متوفر في هذا الإصدار. يرجى الانتظار [Map preview choose another] اختر خريطة +[repeat map horizontally] +كرر الخريطة أفقيًا +[repeat map vertically] +كرر الخريطة عموديًا +[tile x] +تكرار x +[tile y] +تكرار y +[bases] +قواعد +[number of colonies] +عدد المستعمرات، واحدة لكل لاعب؛ زيادته تكرر الخريطة حتى تتسع لها +[bases per colony] +القواعد لكل مستعمرة؛ القاعدة فريق واحد من الخريطة كما رُسمت، بمبانيه ووحداته diff --git a/data/texts.br.txt b/data/texts.br.txt index 36d43b296..bd7252e85 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2210,3 +2210,17 @@ Klikit evit klask en-dro Gortozit mar plij [Map preview choose another] Dibabit ur gartenn +[repeat map horizontally] +Repetir o mapa na horizontal +[repeat map vertically] +Repetir o mapa na vertical +[tile x] +repetição x +[tile y] +repetição y +[bases] +bases +[number of colonies] +Número de colônias, uma por jogador; aumentá-lo repete o mapa até que caibam +[bases per colony] +Bases por colônia; uma base é um time do mapa como foi desenhado, com seus prédios e unidades diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 4df303dff..336140e08 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2222,3 +2222,17 @@ Fes clic per tornar-ho a provar Espera un moment [Map preview choose another] Tria un mapa +[repeat map horizontally] +Repeteix el mapa horitzontalment +[repeat map vertically] +Repeteix el mapa verticalment +[tile x] +repetició x +[tile y] +repetició y +[bases] +bases +[number of colonies] +Nombre de colònies, una per jugador; augmentar-lo repeteix el mapa fins que hi càpiguen +[bases per colony] +Bases per colònia; una base és un equip del mapa tal com està dibuixat, amb els seus edificis i unitats diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 8d662e71f..4a5351d6d 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2214,3 +2214,17 @@ Kliknutím zkuste znovu Čekejte prosím [Map preview choose another] Vyberte mapu +[repeat map horizontally] +Opakovat mapu vodorovně +[repeat map vertically] +Opakovat mapu svisle +[tile x] +opakování x +[tile y] +opakování y +[bases] +základny +[number of colonies] +Počet kolonií, jedna na hráče; zvýšení opakuje mapu, dokud se nevejdou +[bases per colony] +Základny na kolonii; základna je jeden tým mapy tak, jak byla nakreslena, s jeho budovami a jednotkami diff --git a/data/texts.de.txt b/data/texts.de.txt index e34744b56..1ebaab973 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2214,3 +2214,17 @@ Zum Wiederholen klicken Bitte warten [Map preview choose another] Karte auswählen +[repeat map horizontally] +Karte horizontal wiederholen +[repeat map vertically] +Karte vertikal wiederholen +[tile x] +Wiederholung x +[tile y] +Wiederholung y +[bases] +Basen +[number of colonies] +Anzahl der Kolonien, eine pro Spieler; ein höherer Wert wiederholt die Karte, bis sie Platz finden +[bases per colony] +Basen pro Kolonie; eine Basis ist ein Team der gezeichneten Karte mit seinen Gebäuden und Einheiten diff --git a/data/texts.dk.txt b/data/texts.dk.txt index f3f3000a5..4dc2858eb 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2290,3 +2290,17 @@ Klik for at prøve igen Vent venligst [Map preview choose another] Vælg et kort +[repeat map horizontally] +Gentag kortet vandret +[repeat map vertically] +Gentag kortet lodret +[tile x] +gentagelse x +[tile y] +gentagelse y +[bases] +baser +[number of colonies] +Antal kolonier, én pr. spiller; et højere tal gentager kortet, indtil de er plads til dem +[bases per colony] +Baser pr. koloni; en base er ét hold fra kortet, som det er tegnet, med dets bygninger og enheder diff --git a/data/texts.en.txt b/data/texts.en.txt index 6032be710..17ccba19b 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2212,3 +2212,17 @@ Click to retry Please wait [Map preview choose another] Choose a map +[repeat map horizontally] +Repeat map horizontally +[repeat map vertically] +Repeat map vertically +[tile x] +tile x +[tile y] +tile y +[bases] +bases +[number of colonies] +Number of colonies, one per player; raising it repeats the map until they fit +[bases per colony] +Bases per colony; a base is one team of the map as drawn, its buildings and units diff --git a/data/texts.eo.txt b/data/texts.eo.txt index c07afab4d..beafb38ec 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2212,3 +2212,17 @@ Alklaku por reprovi Bonvolu atendi [Map preview choose another] Elektu mapon +[repeat map horizontally] +Ripeti la mapon horizontale +[repeat map vertically] +Ripeti la mapon vertikale +[tile x] +ripeto x +[tile y] +ripeto y +[bases] +bazoj +[number of colonies] +Nombro de kolonioj, unu po ludanto; pliigi ĝin ripetas la mapon ĝis ili enspaciĝas +[bases per colony] +Bazoj po kolonio; bazo estas unu teamo de la mapo kiel desegnite, kun ĝiaj konstruaĵoj kaj unuoj diff --git a/data/texts.es.txt b/data/texts.es.txt index 963a62366..7ca5e1313 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2214,3 +2214,17 @@ Haz clic para reintentar Espera un momento [Map preview choose another] Elige un mapa +[repeat map horizontally] +Repetir el mapa en horizontal +[repeat map vertically] +Repetir el mapa en vertical +[tile x] +repetición x +[tile y] +repetición y +[bases] +bases +[number of colonies] +Número de colonias, una por jugador; aumentarlo repite el mapa hasta que quepan +[bases per colony] +Bases por colonia; una base es un equipo del mapa tal como está dibujado, con sus edificios y unidades diff --git a/data/texts.eu.txt b/data/texts.eu.txt index eab59a3b3..73350f74d 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2224,3 +2224,17 @@ Egin klik berriro saiatzeko Itxaron mesedez [Map preview choose another] Aukeratu mapa bat +[repeat map horizontally] +Errepikatu mapa horizontalki +[repeat map vertically] +Errepikatu mapa bertikalki +[tile x] +errepikapena x +[tile y] +errepikapena y +[bases] +baseak +[number of colonies] +Kolonia kopurua, bat jokalari bakoitzeko; handitzean mapa errepikatzen da denak sartu arte +[bases per colony] +Oinarriak koloniako; oinarria marraztutako maparen talde bat da, bere eraikin eta unitateekin diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 49f077ca8..978a2c6a2 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2212,3 +2212,17 @@ OpenGL در این بیلد موجود نیست. لطفاً صبر کنید [Map preview choose another] یک نقشه انتخاب کنید +[repeat map horizontally] +تکرار نقشه به صورت افقی +[repeat map vertically] +تکرار نقشه به صورت عمودی +[tile x] +تکرار x +[tile y] +تکرار y +[bases] +پایگاه‌ها +[number of colonies] +تعداد مستعمره‌ها، یکی برای هر بازیکن؛ افزایش آن نقشه را تکرار می‌کند تا جا شوند +[bases per colony] +پایگاه برای هر مستعمره؛ هر پایگاه یک تیم از نقشهٔ کشیده‌شده است، با ساختمان‌ها و واحدهایش diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 5fbf0120c..4b91f3033 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2214,3 +2214,17 @@ Yritä uudelleen napsauttamalla Odota hetki [Map preview choose another] Valitse kartta +[repeat map horizontally] +Toista kartta vaakasuunnassa +[repeat map vertically] +Toista kartta pystysuunnassa +[tile x] +toisto x +[tile y] +toisto y +[bases] +tukikohtaa +[number of colonies] +Siirtokuntien määrä, yksi pelaajaa kohden; sen kasvattaminen toistaa karttaa, kunnes ne mahtuvat +[bases per colony] +Tukikohtia siirtokuntaa kohden; tukikohta on piirretyn kartan yksi joukkue rakennuksineen ja yksiköineen diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 4771433e3..0600758b0 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2224,3 +2224,17 @@ Cliquez pour réessayer Veuillez patienter [Map preview choose another] Choisissez une carte +[repeat map horizontally] +Répéter la carte horizontalement +[repeat map vertically] +Répéter la carte verticalement +[tile x] +répétition x +[tile y] +répétition y +[bases] +bases +[number of colonies] +Nombre de colonies, une par joueur ; l'augmenter répète la carte jusqu'à ce qu'elles tiennent +[bases per colony] +Bases par colonie ; une base est une équipe de la carte telle que dessinée, avec ses bâtiments et ses unités diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 85d4ffe57..dfe918ff6 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2284,3 +2284,17 @@ Cortex Παρακαλώ περιμένετε [Map preview choose another] Επιλέξτε χάρτη +[repeat map horizontally] +Επανάληψη χάρτη οριζόντια +[repeat map vertically] +Επανάληψη χάρτη κάθετα +[tile x] +επανάληψη x +[tile y] +επανάληψη y +[bases] +βάσεις +[number of colonies] +Αριθμός αποικιών, μία ανά παίκτη· η αύξησή του επαναλαμβάνει τον χάρτη μέχρι να χωρέσουν +[bases per colony] +Βάσεις ανά αποικία· βάση είναι μία ομάδα του χάρτη όπως σχεδιάστηκε, με τα κτίρια και τις μονάδες της diff --git a/data/texts.hu.txt b/data/texts.hu.txt index f3caeecc4..ba6e049d2 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2214,3 +2214,17 @@ Kattintson az újrapróbáláshoz Kérem, várjon [Map preview choose another] Válasszon térképet +[repeat map horizontally] +Térkép ismétlése vízszintesen +[repeat map vertically] +Térkép ismétlése függőlegesen +[tile x] +ismétlés x +[tile y] +ismétlés y +[bases] +bázis +[number of colonies] +Kolóniák száma, játékosonként egy; növelése addig ismétli a térképet, amíg elférnek +[bases per colony] +Bázisok kolóniánként; egy bázis a megrajzolt térkép egy csapata, épületeivel és egységeivel diff --git a/data/texts.id.txt b/data/texts.id.txt index c43c700ce..613a9ecb2 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2210,3 +2210,17 @@ Klik untuk mencoba lagi Harap tunggu [Map preview choose another] Pilih peta +[repeat map horizontally] +Ulangi peta secara horizontal +[repeat map vertically] +Ulangi peta secara vertikal +[tile x] +ulang x +[tile y] +ulang y +[bases] +markas +[number of colonies] +Jumlah koloni, satu per pemain; menaikkannya mengulang peta sampai semuanya muat +[bases per colony] +Markas per koloni; satu markas adalah satu tim dari peta seperti digambar, beserta bangunan dan unitnya diff --git a/data/texts.it.txt b/data/texts.it.txt index 2835d2406..4827f0f7d 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2276,3 +2276,17 @@ Fai clic per riprovare Attendi [Map preview choose another] Scegli una mappa +[repeat map horizontally] +Ripeti la mappa in orizzontale +[repeat map vertically] +Ripeti la mappa in verticale +[tile x] +ripetizione x +[tile y] +ripetizione y +[bases] +basi +[number of colonies] +Numero di colonie, una per giocatore; aumentarlo ripete la mappa finché non ci stanno +[bases per colony] +Basi per colonia; una base è una squadra della mappa così come è disegnata, con i suoi edifici e le sue unità diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 295462180..446962246 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2210,3 +2210,17 @@ Globulation 2 のカーソルを表示します。 お待ちください [Map preview choose another] マップを選択 +[repeat map horizontally] +マップを横に繰り返す +[repeat map vertically] +マップを縦に繰り返す +[tile x] +繰り返し x +[tile y] +繰り返し y +[bases] +拠点 +[number of colonies] +コロニーの数(プレイヤーごとに1つ)。増やすと収まるまでマップを繰り返します +[bases per colony] +コロニーあたりの拠点数。拠点とは描かれたマップの1チームで、その建物とユニットを含みます diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 7764bee56..1f4def738 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1104,3 +1104,10 @@ [Map preview retry] [Map preview please wait] [Map preview choose another] +[repeat map horizontally] +[repeat map vertically] +[tile x] +[tile y] +[bases] +[number of colonies] +[bases per colony] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index f0cbe6ed6..0f07f375f 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2210,3 +2210,17 @@ Globulation 2 커서를 표시합니다. 잠시 기다려 주세요 [Map preview choose another] 지도 선택 +[repeat map horizontally] +맵을 가로로 반복 +[repeat map vertically] +맵을 세로로 반복 +[tile x] +반복 x +[tile y] +반복 y +[bases] +기지 +[number of colonies] +식민지 수, 플레이어마다 하나; 늘리면 모두 들어갈 때까지 맵을 반복합니다 +[bases per colony] +식민지당 기지 수; 기지는 그려진 맵의 한 팀과 그 건물 및 유닛입니다 diff --git a/data/texts.nl.txt b/data/texts.nl.txt index ee1ebc4fb..e022be101 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2238,3 +2238,17 @@ Klik om opnieuw te proberen Even geduld [Map preview choose another] Kies een kaart +[repeat map horizontally] +Kaart horizontaal herhalen +[repeat map vertically] +Kaart verticaal herhalen +[tile x] +herhaling x +[tile y] +herhaling y +[bases] +bases +[number of colonies] +Aantal kolonies, één per speler; verhogen herhaalt de kaart tot ze passen +[bases per colony] +Bases per kolonie; een basis is één team van de kaart zoals getekend, met zijn gebouwen en eenheden diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 8a74d9662..ced6aa2b8 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2214,3 +2214,17 @@ Kliknij, aby ponowić Proszę czekać [Map preview choose another] Wybierz mapę +[repeat map horizontally] +Powtórz mapę w poziomie +[repeat map vertically] +Powtórz mapę w pionie +[tile x] +powtórzenie x +[tile y] +powtórzenie y +[bases] +bazy +[number of colonies] +Liczba kolonii, jedna na gracza; zwiększenie powtarza mapę, aż się zmieszczą +[bases per colony] +Bazy na kolonię; baza to jedna drużyna mapy w narysowanej postaci, z jej budynkami i jednostkami diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 71f9d02ee..ef0c68ca6 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2220,3 +2220,17 @@ Clique para tentar novamente Aguarde [Map preview choose another] Escolha um mapa +[repeat map horizontally] +Repetir o mapa na horizontal +[repeat map vertically] +Repetir o mapa na vertical +[tile x] +repetição x +[tile y] +repetição y +[bases] +bases +[number of colonies] +Número de colónias, uma por jogador; aumentá-lo repete o mapa até que caibam +[bases per colony] +Bases por colónia; uma base é uma equipa do mapa tal como foi desenhado, com os seus edifícios e unidades diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 2c6d7b7fc..93f91cdfe 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2214,3 +2214,17 @@ Clic pentru a reîncerca Așteaptă [Map preview choose another] Alege o hartă +[repeat map horizontally] +Repetă harta pe orizontală +[repeat map vertically] +Repetă harta pe verticală +[tile x] +repetare x +[tile y] +repetare y +[bases] +baze +[number of colonies] +Numărul de colonii, câte una pentru fiecare jucător; mărirea lui repetă harta până încap +[bases per colony] +Baze pe colonie; o bază este o echipă a hărții așa cum a fost desenată, cu clădirile și unitățile ei diff --git a/data/texts.ru.txt b/data/texts.ru.txt index b0271b92f..756ed7d35 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2212,3 +2212,17 @@ OpenGL недоступен в этой сборке. Подождите [Map preview choose another] Выберите карту +[repeat map horizontally] +Повторить карту по горизонтали +[repeat map vertically] +Повторить карту по вертикали +[tile x] +повтор x +[tile y] +повтор y +[bases] +базы +[number of colonies] +Число колоний, по одной на игрока; при увеличении карта повторяется, пока все не поместятся +[bases per colony] +Баз на колонию; база — это одна команда карты в нарисованном виде, с её зданиями и юнитами diff --git a/data/texts.si.txt b/data/texts.si.txt index 31b1f68d8..d1a40edb1 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2214,3 +2214,17 @@ Kliknite za ponovni poskus Počakajte [Map preview choose another] Izberite zemljevid +[repeat map horizontally] +Ponovi zemljevid vodoravno +[repeat map vertically] +Ponovi zemljevid navpično +[tile x] +ponovitev x +[tile y] +ponovitev y +[bases] +baze +[number of colonies] +Število kolonij, ena na igralca; povečanje ponavlja zemljevid, dokler ne gredo vse nanj +[bases per colony] +Baze na kolonijo; baza je ena ekipa zemljevida, kot je narisan, z njenimi zgradbami in enotami diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 4f7b7b56c..5b4d66ab2 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2222,3 +2222,17 @@ Kliknutím skúste znova Čakajte, prosím [Map preview choose another] Vyberte mapu +[repeat map horizontally] +Opakovať mapu vodorovne +[repeat map vertically] +Opakovať mapu zvislo +[tile x] +opakovanie x +[tile y] +opakovanie y +[bases] +základne +[number of colonies] +Počet kolónií, jedna na hráča; zvýšenie opakuje mapu, kým sa nezmestia +[bases per colony] +Základne na kolóniu; základňa je jeden tím mapy tak, ako bola nakreslená, s jeho budovami a jednotkami diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 8ae6f66af..64833271a 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2214,3 +2214,17 @@ OpenGL није доступан у овој верзији. Сачекајте [Map preview choose another] Изаберите мапу +[repeat map horizontally] +Понови мапу водоравно +[repeat map vertically] +Понови мапу усправно +[tile x] +понављање x +[tile y] +понављање y +[bases] +базе +[number of colonies] +Број колонија, једна по играчу; повећање понавља мапу док све не стану +[bases per colony] +Базе по колонији; база је једна екипа мапе онако како је нацртана, са својим зградама и јединицама diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 58de86fee..98f2ad5f9 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2224,3 +2224,17 @@ Klicka för att försöka igen Vänta [Map preview choose another] Välj en karta +[repeat map horizontally] +Upprepa kartan vågrätt +[repeat map vertically] +Upprepa kartan lodrätt +[tile x] +upprepning x +[tile y] +upprepning y +[bases] +baser +[number of colonies] +Antal kolonier, en per spelare; att öka det upprepar kartan tills de får plats +[bases per colony] +Baser per koloni; en bas är ett lag från kartan som den är ritad, med dess byggnader och enheter diff --git a/data/texts.tr.txt b/data/texts.tr.txt index c394dab84..f489964d6 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2222,3 +2222,17 @@ Yeniden denemek için tıklayın Lütfen bekleyin [Map preview choose another] Bir harita seçin +[repeat map horizontally] +Haritayı yatay olarak tekrarla +[repeat map vertically] +Haritayı dikey olarak tekrarla +[tile x] +tekrar x +[tile y] +tekrar y +[bases] +üs +[number of colonies] +Koloni sayısı, oyuncu başına bir; artırmak, hepsi sığana kadar haritayı tekrarlar +[bases per colony] +Koloni başına üs; üs, haritanın çizildiği hâliyle bir takımı, binaları ve birimleriyle birlikte diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 129694d51..936771f48 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2210,3 +2210,17 @@ OpenGL недоступний у цій збірці. Зачекайте [Map preview choose another] Виберіть мапу +[repeat map horizontally] +Повторити мапу по горизонталі +[repeat map vertically] +Повторити мапу по вертикалі +[tile x] +повтор x +[tile y] +повтор y +[bases] +бази +[number of colonies] +Кількість колоній, по одній на гравця; збільшення повторює мапу, доки всі не вмістяться +[bases per colony] +Баз на колонію; база — це одна команда мапи в намальованому вигляді, з її будівлями та юнітами diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 1e28d5ee8..9f00b6dbe 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2210,3 +2210,17 @@ Nhấp để thử lại Vui lòng đợi [Map preview choose another] Chọn bản đồ +[repeat map horizontally] +Lặp bản đồ theo chiều ngang +[repeat map vertically] +Lặp bản đồ theo chiều dọc +[tile x] +lặp x +[tile y] +lặp y +[bases] +căn cứ +[number of colonies] +Số thuộc địa, mỗi người chơi một; tăng lên sẽ lặp bản đồ cho đến khi đủ chỗ +[bases per colony] +Căn cứ mỗi thuộc địa; một căn cứ là một đội của bản đồ như đã vẽ, cùng các công trình và đơn vị của nó diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 6e5eef13c..ca4be8be8 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2210,3 +2210,17 @@ OpenGL 在此版本中不可用。 请稍候 [Map preview choose another] 选择地图 +[repeat map horizontally] +水平重复地图 +[repeat map vertically] +垂直重复地图 +[tile x] +重复 x +[tile y] +重复 y +[bases] +基地 +[number of colonies] +殖民地数量,每位玩家一个;增加后地图会重复直到容纳所有殖民地 +[bases per colony] +每个殖民地的基地数;一个基地是地图原样中的一支团队及其建筑和单位 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 792e662f8..e87059091 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2286,3 +2286,17 @@ OpenGL 在此版本中不可用。 請稍候 [Map preview choose another] 選擇地圖 +[repeat map horizontally] +水平重複地圖 +[repeat map vertically] +垂直重複地圖 +[tile x] +重複 x +[tile y] +重複 y +[bases] +基地 +[number of colonies] +殖民地數量,每位玩家一個;增加後地圖會重複直到容納所有殖民地 +[bases per colony] +每個殖民地的基地數;一個基地是地圖原樣中的一支團隊及其建築和單位 diff --git a/src/ChooseMapScreen.cpp b/src/ChooseMapScreen.cpp index b66469843..c5635e30a 100644 --- a/src/ChooseMapScreen.cpp +++ b/src/ChooseMapScreen.cpp @@ -2,6 +2,10 @@ // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière #include "ChooseMapScreen.h" +#include +#include +#include "Team.h" +#include "MapTiling.h" #include "GUIGlob2FileList.h" #include "GUIMapPreview.h" #include "GlobalContainer.h" @@ -58,16 +62,33 @@ ChooseMapScreen::ChooseMapScreen(const char *directory, const char *extension, b title = new Text(0, 18, ALIGN_FILL, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[choose game]")); } addWidget(title); - mapName=new Text(440, 60+128+25, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); - addWidget(mapName); - mapInfo=new Text(440, 60+128+50, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); + // the right column, under the preview: the map's teams or bases, its size, then the repeat controls + const int infoTop = 60+128+22; + mapInfo=new Text(440, infoTop, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); addWidget(mapInfo); - mapVersion=new Text(440, 60+128+75, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); - addWidget(mapVersion); - mapSize=new Text(440, 60+128+100, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); + mapSize=new Text(440, infoTop+22, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); addWidget(mapSize); - mapDate=new Text(440, 60+128+125, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); + mapDate=new Text(440, infoTop+44, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", "", 180); addWidget(mapDate); + const int controlTop = infoTop+48, controlPitch = 26; + repeatXText=new Text(440, controlTop+2, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[tile x]"), 100); + addWidget(repeatXText); + repeatX=new Number(540, controlTop, 80, 22, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 16, "standard", Toolkit::getStringTable()->getString("[repeat map horizontally]"), "standard"); + addWidget(repeatX); + repeatYText=new Text(440, controlTop+controlPitch+2, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[tile y]"), 100); + addWidget(repeatYText); + repeatY=new Number(540, controlTop+controlPitch, 80, 22, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 16, "standard", Toolkit::getStringTable()->getString("[repeat map vertically]"), "standard"); + addWidget(repeatY); + teamCountText=new Text(440, controlTop+2*controlPitch+2, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[colonies]"), 100); + addWidget(teamCountText); + teamCount=new Number(540, controlTop+2*controlPitch, 80, 22, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 16, "standard", Toolkit::getStringTable()->getString("[number of colonies]"), "standard"); + addWidget(teamCount); + coloniesPerTeamText=new Text(440, controlTop+3*controlPitch+2, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[bases]"), 100); + addWidget(coloniesPerTeamText); + coloniesPerTeam=new Number(540, controlTop+3*controlPitch, 80, 22, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 16, "standard", Toolkit::getStringTable()->getString("[bases per colony]"), "standard"); + addWidget(coloniesPerTeam); + repeatX->visible = repeatY->visible = teamCount->visible = coloniesPerTeam->visible = false; + repeatXText->visible = repeatYText->visible = teamCountText->visible = coloniesPerTeamText->visible = false; if(alternateDirectory) { @@ -100,7 +121,9 @@ void ChooseMapScreen::onAction(Widget *source, Action action, int par1, int par2 try { + selectedMapFile = mapFileName; mapPreview->setMapThumbnail(mapFileName.c_str()); + previewTiled = false; auto stream = std::unique_ptr(new BinaryInputStream(Toolkit::getFileManager()->openInputStreamBackend(mapFileName))); if (stream->isEndOfStream()) @@ -112,18 +135,25 @@ void ChooseMapScreen::onAction(Widget *source, Action action, int par1, int par2 if (verbose) std::cout << "ChooseMapScreen::onAction : loading map " << mapFileName << std::endl; validMapSelected = mapHeader.load(stream.get()); - if (!validMapSelected) selectedType = NONE; - mapHeader.setMapName(glob2FilenameToName(mapFileName)); if (validMapSelected) { - updateMapInformation(); - + selectedType = activeType(); + sourceMapHeader = mapHeader; + if (selectedType == MAP) + { + if (!teamsPicked) + tileTeams = 0; + if (!basesPicked) + tileColonies = 0; + MapTiling::adjust(infoOfMap(mapFileName), tileX, tileY, tileTeams, tileColonies); + } + updateTilingControls(); + applyTiling(); + sortMapList(); time_t mtime = Toolkit::getFileManager()->mtime(mapFileName); mapDate->setText(ctime(&mtime)); - - selectedType = activeType(); } else std::cerr << "ChooseMapScreen::onAction : invalid map header for map " << mapFileName << std::endl; @@ -140,21 +170,69 @@ void ChooseMapScreen::onAction(Widget *source, Action action, int par1, int par2 else { mapDate->setText(""); - mapVersion->setText(""); mapInfo->setText(""); mapSize->setText(""); - mapName->setText(""); + selectedMapFile.clear(); mapPreview->setMapThumbnail(""); + previewTiled = false; validMapSelected = false; + repeatX->visible = repeatY->visible = teamCount->visible = coloniesPerTeam->visible = false; + repeatXText->visible = repeatYText->visible = teamCountText->visible = coloniesPerTeamText->visible = false; + } + } + else if (action == NUMBER_ELEMENT_SELECTED && (source == repeatX || source == repeatY || source == teamCount || source == coloniesPerTeam)) + { + if (source == repeatX || source == repeatY) + { + tileX = repeatX->get(); + tileY = repeatY->get(); + // a smaller repeat holds fewer bases: the counts picked before shrink to what fits + const int colonies = MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY); + tileTeams = std::max(1, std::min(tileTeams, std::min(Team::MAX_COUNT, colonies))); + tileColonies = std::max(1, std::min(tileColonies, colonies / tileTeams)); + } + else if (source == teamCount) + { + tileTeams = teamCount->get(); + teamsPicked = true; + } + else + { + tileColonies = coloniesPerTeam->get(); + basesPicked = true; + } + if (source == teamCount) + { + // the colony count comes first: the repeat grows to make room for it + const int wanted = tileTeams; + MapTiling::adjust(infoOfMap(activeFileList()->listToFile(activeFileList()->get())), tileX, tileY, tileTeams, tileColonies); + if (tileTeams != wanted) + tileTeams = wanted; } + updateTilingControls(); + applyTiling(); + sortMapList(); } else if ((action == BUTTON_RELEASED) || (action == BUTTON_SHORTCUT)) { if (source == ok) { // we accept only if a valid map is selected - if (validMapSelected) - endExecute(OK); + if (!validMapSelected) + return; + if (tilingActive()) + { + // the repeated map becomes a map file of its own, so the game and its clients see an ordinary map + const int teamsHeld = std::min(tileTeams, std::min(Team::MAX_COUNT, MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY))); + MapHeader written = MapTiling::writeTiledMap(sourceMapHeader, tileX, tileY, teamsHeld, tileColonies); + if (written.getNumberOfTeams() < 1) + { + GAGGUI::MessageBox(globalContainer->gfx, "standard", GAGGUI::MB_ONEBUTTON, Toolkit::getStringTable()->getString("[ERROR_CANT_LOAD_MAP]"), Toolkit::getStringTable()->getString("[ok]")); + return; + } + mapHeader = written; + } + endExecute(OK); } else if (source == cancel) { @@ -186,14 +264,19 @@ void ChooseMapScreen::onAction(Widget *source, Action action, int par1, int par2 void ChooseMapScreen::updateMapInformation() { - // update map name & info - mapName->setText(mapHeader.getMapName()); + // games and replays report their teams; a map reports the bases it holds once repeated std::string textTemp; - textTemp = FormattableString("%0%1").arg(mapHeader.getNumberOfTeams()).arg(Toolkit::getStringTable()->getString("[teams]")); + const MapTiling::MapInfo& source = infoOfMap(selectedMapFile); + if (selectedType == MAP && source.valid) + textTemp = FormattableString("%0 %1").arg(MapTiling::colonyCount(source.header.getNumberOfTeams(), tileX, tileY)).arg(Toolkit::getStringTable()->getString("[bases]")); + else + textTemp = FormattableString("%0%1").arg(mapHeader.getNumberOfTeams()).arg(Toolkit::getStringTable()->getString("[teams]")); mapInfo->setText(textTemp); - textTemp = FormattableString("%0 %1.%2").arg(Toolkit::getStringTable()->getString("[Version]")).arg(mapHeader.getVersionMajor()).arg(mapHeader.getVersionMinor()); - mapVersion->setText(textTemp); - textTemp = FormattableString("%0 x %1").arg(mapPreview->getLastWidth()).arg(mapPreview->getLastHeight()); + const int w = source.valid ? source.w : mapPreview->getLastWidth(), h = source.valid ? source.h : mapPreview->getLastHeight(); + if (tileX > 1 || tileY > 1) + textTemp = FormattableString("%0 x %1 (%2 x %3)").arg(w * tileX).arg(h * tileY).arg(w).arg(h); + else + textTemp = FormattableString("%0 x %1").arg(w).arg(h); mapSize->setText(textTemp); // call subclass handler @@ -201,6 +284,130 @@ void ChooseMapScreen::updateMapInformation() } +void ChooseMapScreen::updateTilingControls() +{ + const bool tileable = validMapSelected && selectedType == MAP && sourceMapHeader.getNumberOfTeams() > 0; + repeatX->visible = repeatY->visible = teamCount->visible = coloniesPerTeam->visible = tileable; + repeatXText->visible = repeatYText->visible = teamCountText->visible = coloniesPerTeamText->visible = tileable; + mapDate->visible = !tileable; + if (!tileable) + { + tileX = tileY = 1; + tileTeams = 0; + tileColonies = 0; + return; + } + // a cleared Number keeps its old index, so it is reset before the value is picked + repeatX->clear(); + const MapTiling::MapInfo& source = infoOfMap(selectedMapFile); + for (int f : MapTiling::repeatOptions(source.w)) + repeatX->add(f); + repeatX->setNth(0); + repeatX->set(tileX); + tileX = repeatX->get(); + repeatY->clear(); + for (int f : MapTiling::repeatOptions(source.h)) + repeatY->add(f); + repeatY->setNth(0); + repeatY->set(tileY); + tileY = repeatY->get(); + // the colony count offers the whole range; a map that cannot hold it sorts as a violator + const int colonies = MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY); + const int maxTeams = std::min(Team::MAX_COUNT, colonies); + if (!teamsPicked || tileTeams < 1) + tileTeams = maxTeams; + teamCount->clear(); + for (int t = 1; t <= Team::MAX_COUNT; t++) + teamCount->add(t); + teamCount->setNth(0); + teamCount->set(tileTeams); + tileTeams = teamCount->get(); + // bases per colony: as many as fit unless the user asked for fewer + const int maxSwarms = std::max(1, colonies / std::min(tileTeams, maxTeams)); + if (!basesPicked || tileColonies < 1) + tileColonies = maxSwarms; + coloniesPerTeam->clear(); + for (int c = 1; c <= maxSwarms; c++) + coloniesPerTeam->add(c); + coloniesPerTeam->setNth(0); + coloniesPerTeam->set(std::min(tileColonies, maxSwarms)); + tileColonies = coloniesPerTeam->get(); +} + +const MapTiling::MapInfo& ChooseMapScreen::infoOfMap(const std::string& fileName) +{ + auto it = mapInfos.find(fileName); + if (it == mapInfos.end()) + it = mapInfos.insert(std::make_pair(fileName, MapTiling::readMapInfo(fileName))).first; + return it->second; +} + +void ChooseMapScreen::sortMapList() +{ + if (activeType() != MAP) + return; + Glob2FileList* list = activeFileList(); + const std::string selected = list->selection() ? list->get() : ""; + std::vector dirs, fitting, others; + for (size_t i = 0; i < list->getCount(); i++) + { + const std::string& entry = list->getText(i); + if (!entry.empty() && entry[entry.size()-1] == '/') + dirs.push_back(entry); + else if (MapTiling::fits(infoOfMap(list->listToFile(entry)), tileX, tileY, teamsPicked ? tileTeams : 0, basesPicked ? tileColonies : 0)) + fitting.push_back(entry); + else + others.push_back(entry); + } + list->clear(); + for (const std::string& e : dirs) list->addText(e); + for (const std::string& e : fitting) list->addText(e); + for (const std::string& e : others) list->addText(e); + for (size_t i = 0; i < list->getCount(); i++) + if (list->getText(i) == selected) + list->setSelection(i); +} + +bool ChooseMapScreen::tilingActive() const +{ + const int teamsHeld = std::min(tileTeams, std::min(Team::MAX_COUNT, MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY))); + return selectedType == MAP && (MapTiling::isActive(tileX, tileY, teamsHeld, sourceMapHeader.getNumberOfTeams()) + || tileColonies * teamsHeld < MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY)); +} + +void ChooseMapScreen::applyTiling() +{ + if (!validMapSelected) + return; + if (selectedType == MAP) + { + const int teamsHeld = std::min(tileTeams, std::min(Team::MAX_COUNT, MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY))); + mapHeader = MapTiling::tiledHeader(sourceMapHeader, tileX, tileY, teamsHeld); + } + updatePreview(); + updateMapInformation(); +} + +void ChooseMapScreen::updatePreview() +{ + if (tilingActive()) + { + const int teamsHeld = std::min(tileTeams, std::min(Team::MAX_COUNT, MapTiling::colonyCount(sourceMapHeader.getNumberOfTeams(), tileX, tileY))); + MapThumbnail thumbnail; + if (MapTiling::tiledThumbnail(sourceMapHeader, tileX, tileY, teamsHeld, tileColonies, thumbnail)) + { + mapPreview->setMapThumbnail(thumbnail); + previewTiled = true; + return; + } + } + if (previewTiled) + { + mapPreview->setMapThumbnail(selectedMapFile); + previewTiled = false; + } +} + MapHeader& ChooseMapScreen::getMapHeader() { return mapHeader; diff --git a/src/ChooseMapScreen.h b/src/ChooseMapScreen.h index 25d18b998..fde7d37c2 100644 --- a/src/ChooseMapScreen.h +++ b/src/ChooseMapScreen.h @@ -4,6 +4,8 @@ #pragma once #include "MapHeader.h" +#include "MapTiling.h" +#include #include "GameHeader.h" #include "Glob2Screen.h" #include @@ -98,8 +100,36 @@ class ChooseMapScreen : public Glob2Screen Glob2FileList *alternateFileList = nullptr; //! The widget that will show a preview of the selection map MapPreview *mapPreview; - //! The textual informations about the selected map - Text *mapName, *mapInfo, *mapVersion, *mapSize, *mapDate; + //! The textual informations about the selected map: teams or bases, size, date + Text *mapInfo, *mapSize, *mapDate; + //! Repeat factors, colony count and bases per colony for a map, see MapTiling.h; hidden for games and replays + Number *repeatX, *repeatY, *teamCount, *coloniesPerTeam; + //! Captions beside the four controls + Text *repeatXText, *repeatYText, *teamCountText, *coloniesPerTeamText; + //! The file of the selected map, key into mapInfos + std::string selectedMapFile; + //! True while the preview shows the repeated map rather than the file's own thumbnail + bool previewTiled = false; + //! Show the repeated map in the preview when the controls ask for one, the plain map otherwise + void updatePreview(); + //! Header and size of every map in the list, read once + std::map mapInfos; + //! Header and size of a listed map, read on first use + const MapTiling::MapInfo& infoOfMap(const std::string& fileName); + //! Reorder the active list: maps that allow the current numbers first, the others after + void sortMapList(); + //! The header of the selected map as it is in the file; mapHeader describes the repeated map + MapHeader sourceMapHeader; + //! Fill the repeat and team controls for the selected map + void updateTilingControls(); + //! The repeat factors and team count currently chosen; the widgets only mirror them + int tileX = 1, tileY = 1, tileTeams = 0, tileColonies = 0; + //! True once the user set the colony count or the bases per colony; until then they follow the map + bool teamsPicked = false, basesPicked = false; + //! Derive mapHeader from the controls + void applyTiling(); + //! True when the controls ask for a repeated map + bool tilingActive() const; //! True when the selected map is valid bool validMapSelected; //! Default type diff --git a/src/Game.h b/src/Game.h index 581fce447..8ba7611d4 100644 --- a/src/Game.h +++ b/src/Game.h @@ -219,6 +219,8 @@ class Game //! If a team is uncontrolled (playerMask == 0), remove units and buildings from map void clearingUncontrolledTeams(void); void regenerateDiscoveryMap(void); + ///Repeats the loaded map rx by ry times and deals its colonies round robin to teamCount fresh teams, see MapTiling.h + bool tileForPlay(int rx, int ry, int teamCount, int coloniesPerTeam); Unit *addUnit(int x, int y, int team, int type, int level, int delta, int dx, int dy); Building *addBuilding(int x, int y, int typeNum, int teamNumber, Sint32 unitWorking = 1, Sint32 unitWorkingFuture = 1); diff --git a/src/Game_tiling.cpp b/src/Game_tiling.cpp new file mode 100644 index 000000000..25b5253f1 --- /dev/null +++ b/src/Game_tiling.cpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Repeating a map at game setup: see MapTiling.h. +#include "Game.h" +#include "Building.h" +#include "BuildingType.h" +#include "GlobalContainer.h" +#include "MapTiling.h" +#include "Team.h" +#include "Unit.h" +#include "UnitType.h" + +#include + +namespace +{ + // positions are offsets from the colony's anchor, the shortest way round the source torus + struct BuildingTemplate + { + int x, y, typeNum, hp, bullets, maxUnitWorking, maxUnitWorkingFuture, maxUnitInside, priority, unitStayRange, minLevelToFlag; + Uint32 receiveResourceMask, sendResourceMask; + Sint32 resources[MAX_NB_RESOURCES]; + Sint32 ratio[NB_UNIT_TYPE]; + bool clearingResources[BASIC_COUNT]; + }; + + struct UnitTemplate + { + int x, y, typeNum, hp, hungry, hungriness, experience, experienceLevel; + Uint32 fruitMask, fruitCount; + Unit::Medical medical; + Sint32 level[NB_ABILITY]; + }; + + struct ColonyTemplate + { + int anchorX = -1, anchorY = -1; + std::vector buildings; + std::vector units; + }; + + //! Signed offset from a to v on a ring of `size`, taking the shorter way round + int wrapOffset(int v, int a, int size) + { + int d = (v - a) % size; + if (d < 0) + d += size; + if (d >= size / 2) + d -= size; + return d; + } +} + +bool Game::tileForPlay(int rx, int ry, int teamCount, int coloniesPerTeam) +{ + const int mapTeams = mapHeader.getNumberOfTeams(); + const int total = MapTiling::colonyCount(mapTeams, rx, ry); + if (teamCount < 1) + teamCount = std::min(Team::MAX_COUNT, total); + if (mapTeams < 1 || rx < 1 || ry < 1 || teamCount > Team::MAX_COUNT || teamCount > total) + return false; + if (map.getW() * rx > MapTiling::MAX_MAP_SIDE || map.getH() * ry > MapTiling::MAX_MAP_SIDE) + return false; + const MapHeader tiledHeader = MapTiling::tiledHeader(mapHeader, rx, ry, teamCount); + + // every colony of the map as it was loaded, each element relative to the colony's anchor: + // a map wraps, so a unit may stand across the edge from its swarm, and the copy it belongs + // to is the one that keeps it near that swarm + const int w0 = map.getW(), h0 = map.getH(); + std::vector colonies(mapTeams); + for (int t = 0; t < mapTeams; t++) + { + ColonyTemplate& colony = colonies[t]; + // the anchor is the first swarm, else the first building, else the first unit + for (int pass = 0; pass < 2 && colony.anchorX < 0; pass++) + for (int i = 0; i < Building::MAX_COUNT && colony.anchorX < 0; i++) + { + const Building* b = teams[t]->myBuildings[i]; + if (b && b->buildingState == Building::ALIVE && (pass == 1 || b->type->unitProductionTime)) + { + colony.anchorX = b->posX; + colony.anchorY = b->posY; + } + } + for (int i = 0; i < Unit::MAX_COUNT && colony.anchorX < 0; i++) + if (teams[t]->myUnits[i] && !teams[t]->myUnits[i]->isDead) + { + colony.anchorX = teams[t]->myUnits[i]->posX; + colony.anchorY = teams[t]->myUnits[i]->posY; + } + if (colony.anchorX < 0) + continue; + for (int i = 0; i < Building::MAX_COUNT; i++) + { + const Building* b = teams[t]->myBuildings[i]; + if (!b || b->buildingState != Building::ALIVE) + continue; + BuildingTemplate bt = { wrapOffset(b->posX, colony.anchorX, w0), wrapOffset(b->posY, colony.anchorY, h0), b->typeNum, b->hp, b->bullets, b->maxUnitWorking, b->getMaxUnitWorkingFuture(), b->maxUnitInside, b->priority, b->unitStayRange, b->minLevelToFlag, b->receiveResourceMask, b->sendResourceMask, {}, {} }; + for (int r = 0; r < MAX_NB_RESOURCES; r++) + bt.resources[r] = b->resources[r]; + for (int u = 0; u < NB_UNIT_TYPE; u++) + bt.ratio[u] = b->ratio[u]; + for (int r = 0; r < BASIC_COUNT; r++) + bt.clearingResources[r] = b->clearingResources[r]; + colonies[t].buildings.push_back(bt); + } + for (int i = 0; i < Unit::MAX_COUNT; i++) + { + const Unit* u = teams[t]->myUnits[i]; + if (!u || u->isDead) + continue; + // units inside a building are not on the map and are not carried over + if (map.getGroundUnit(u->posX, u->posY) != u->gid && map.getAirUnit(u->posX, u->posY) != u->gid) + continue; + UnitTemplate ut = { wrapOffset(u->posX, colony.anchorX, w0), wrapOffset(u->posY, colony.anchorY, h0), u->typeNum, u->hp, u->hungry, u->hungriness, u->experience, u->experienceLevel, u->fruitMask, u->fruitCount, u->medical, {} }; + for (int a = 0; a < NB_ABILITY; a++) + ut.level[a] = u->level[a]; + colonies[t].units.push_back(ut); + } + } + + // the areas each colony's team painted, by tile of the map as loaded; Map::tile drops the + // per-team masks, and every copy paints them again for the team its colony is dealt to + struct PaintedTile + { + int x, y; + bool forbidden, guard, clear; + }; + std::vector> painted(mapTeams); + for (int y = 0; y < h0; y++) + for (int x = 0; x < w0; x++) + for (int t = 0; t < mapTeams; t++) + { + const Uint32 mask = Team::teamNumberToMask(t); + const bool forbidden = map.isForbidden(x, y, mask), guard = map.isGuardArea(x, y, mask), clear = map.isClearArea(x, y, mask); + if (forbidden || guard || clear) + painted[t].push_back({x, y, forbidden, guard, clear}); + } + + while (mapHeader.getNumberOfTeams() > 0) + removeTeam(TEAM_POS_END); + map.tile(rx, ry); + map.setGame(this); + for (int k = 0; k < teamCount; k++) + { + addTeam(TEAM_POS_END); + const BaseTeam& initial = tiledHeader.getBaseTeam(k); + teams[k]->teamNumber = initial.teamNumber; + teams[k]->numberOfPlayer = initial.numberOfPlayer; + teams[k]->playersMask = initial.playersMask; + teams[k]->setCorrectColor(initial.color); + teams[k]->setCorrectMasks(); + mapHeader.getBaseTeam(k) = tiledHeader.getBaseTeam(k); + } + + int n = 0; + for (int j = 0; j < ry; j++) + for (int i = 0; i < rx; i++) + for (int t = 0; t < mapTeams; t++, n++) + { + const int k = MapTiling::teamForColony(n, total, teamCount, coloniesPerTeam); + if (k < 0) + continue; + // the anchor of this copy; offsets wrap on the repeated map + const int ax = colonies[t].anchorX + i * w0, ay = colonies[t].anchorY + j * h0; + const int W = map.getW(), H = map.getH(); + auto wrapX = [&](int v) { return ((v % W) + W) % W; }; + auto wrapY = [&](int v) { return ((v % H) + H) % H; }; + for (const BuildingTemplate& bt : colonies[t].buildings) + { + Building* b = addBuilding(wrapX(ax + bt.x), wrapY(ay + bt.y), bt.typeNum, k, bt.maxUnitWorking, bt.maxUnitWorkingFuture); + if (!b) + continue; + b->hp = bt.hp; + b->bullets = bt.bullets; + b->maxUnitInside = bt.maxUnitInside; + b->minLevelToFlag = bt.minLevelToFlag; + b->receiveResourceMask = bt.receiveResourceMask; + b->sendResourceMask = bt.sendResourceMask; + b->priority = bt.priority; + b->unitStayRange = bt.unitStayRange; + for (int r = 0; r < MAX_NB_RESOURCES; r++) + b->resources[r] = bt.resources[r]; + for (int u = 0; u < NB_UNIT_TYPE; u++) + b->ratio[u] = bt.ratio[u]; + for (int r = 0; r < BASIC_COUNT; r++) + b->clearingResources[r] = bt.clearingResources[r]; + if (!teams[k]->startPosSet && b->type->unitProductionTime) + { + teams[k]->startPosX = b->posX; + teams[k]->startPosY = b->posY; + teams[k]->startPosSet = 1; + } + } + for (const PaintedTile& p : painted[t]) + { + const int x = p.x + i * w0, y = p.y + j * h0; + if (p.forbidden) + map.addForbidden(x, y, k); + if (p.guard) + map.addGuardArea(x, y, k); + if (p.clear) + map.addClearArea(x, y, k); + } + for (const UnitTemplate& ut : colonies[t].units) + { + Unit* u = addUnit(wrapX(ax + ut.x), wrapY(ay + ut.y), k, ut.typeNum, ut.level[WALK], 0, 0, 0); + if (!u) + continue; + for (int a = 0; a < NB_ABILITY; a++) + { + u->level[a] = ut.level[a]; + u->performance[a] = teams[k]->race.getUnitType(ut.typeNum, ut.level[a])->performance[a]; + } + u->hp = ut.hp; + u->hungry = ut.hungry; + u->hungriness = ut.hungriness; + u->experience = ut.experience; + u->experienceLevel = ut.experienceLevel; + u->fruitMask = ut.fruitMask; + u->fruitCount = ut.fruitCount; + u->medical = ut.medical; + } + } + + for (int k = 0; k < teamCount; k++) + { + // addBuilding already listed the virtual buildings; createLists insists on filling that list itself + teams[k]->virtualBuildings.clear(); + teams[k]->createLists(); + } + regenerateDiscoveryMap(); + return true; +} diff --git a/src/Glob2.cpp b/src/Glob2.cpp index f0557ef10..93ff6fc8d 100644 --- a/src/Glob2.cpp +++ b/src/Glob2.cpp @@ -13,6 +13,9 @@ #include "EditorMainMenu.h" #include "Engine.h" #include "Game.h" +#include "Building.h" +#include "Unit.h" +#include "FormatableString.h" #include "GenerationContext.h" #include "GenerationService.h" #include "GeneratorRegistry.h" @@ -236,8 +239,10 @@ static int dumpResources(const std::string& mapName) if (map.getResource(x, y).type == WHEAT) { wheatCount++; - if (x < minX) minX = x; if (x > maxX) maxX = x; - if (y < minY) minY = y; if (y > maxY) maxY = y; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; } const int teamCount = game.mapHeader.getNumberOfTeams(); @@ -406,6 +411,58 @@ static int dumpWheatPlan(const std::string& mapName, int team) } return 0; } + +static void dumpTeams(const Game& game) +{ + for (int t = 0; t < game.mapHeader.getNumberOfTeams(); t++) + { + Team* team = game.teams[t]; + int units = 0, buildings = 0, swarmCount = 0; + std::string where; + for (int i = 0; i < Unit::MAX_COUNT; i++) + if (team->myUnits[i] && !team->myUnits[i]->isDead) + { + units++; + where += FormattableString(" u%0,%1").arg(team->myUnits[i]->posX).arg(team->myUnits[i]->posY); + } + for (int i = 0; i < Building::MAX_COUNT; i++) + if (Building* b = team->myBuildings[i]) + { + buildings++; + if (b->type->unitProductionTime) + { + swarmCount++; + where += FormattableString(" (%0,%1)").arg(b->posX).arg(b->posY); + } + } + std::cout << " team " << t << " units=" << units << " buildings=" << buildings << " swarms=" << swarmCount << where << std::endl; + } +} + +// Headless check of a repeated map: -dump-tiled +static int dumpTiled(const std::string& mapName, int rx, int ry, int colonies, int swarms) +{ + using namespace GAGCore; + InputStream* stream = new BinaryInputStream(Toolkit::getFileManager()->openInputStreamBackend(mapName)); + if (stream->isEndOfStream()) + { + std::cerr << "dump-tiled: cannot open " << mapName << std::endl; + delete stream; + return 1; + } + Game game(NULL); + bool ok = game.load(stream); + delete stream; + if (!ok || !game.tileForPlay(rx, ry, colonies, swarms)) + { + std::cerr << "dump-tiled: failed on " << mapName << std::endl; + return 1; + } + std::cout << "Map " << mapName << " " << rx << "x" << ry << " : " << game.map.getW() << "x" << game.map.getH() + << ", teams=" << game.mapHeader.getNumberOfTeams() << std::endl; + dumpTeams(game); + return 0; +} #endif // !YOG_SERVER_ONLY int Glob2::run(int argc, char *argv[]) @@ -439,6 +496,12 @@ int Glob2::run(int argc, char *argv[]) delete globalContainer; return ret; } + else if (strcmp(argv[ai], "-dump-tiled") == 0 && ai + 5 < argc) + { + int ret = dumpTiled(argv[ai + 1], atoi(argv[ai + 2]), atoi(argv[ai + 3]), atoi(argv[ai + 4]), atoi(argv[ai + 5])); + delete globalContainer; + return ret; + } #endif // !YOG_SERVER_ONLY if ( SDLNet_Init() < 0 ) diff --git a/src/MapTiling.cpp b/src/MapTiling.cpp new file mode 100644 index 000000000..8e50164c4 --- /dev/null +++ b/src/MapTiling.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MapTiling.h" +#include "MapHeader.h" +#include "Game.h" +#include "MapThumbnail.h" +#include "Team.h" +#include "Utilities.h" +#include "BinaryStream.h" +#include "FormatableString.h" +#include "Toolkit.h" +#include "FileManager.h" +#include +#include +#include +#include + +namespace +{ + //! A path under the system temp directory for a generated tiled map, not under any + //! directory FileManager or ChooseMapScreen searches, so tiled maps never show up + //! alongside the user's own maps. A per-process counter keeps concurrent tilings + //! (e.g. host and a local test client) from colliding on the same file. + std::string tempTiledMapPath(const std::string& name) + { + static int counter = 0; + std::string fileName = name; + std::replace_if(fileName.begin(), fileName.end(), [](char c) { return c == ' ' || c == '\t' || c == '/' || c == '\\'; }, '_'); +#ifdef WIN32 + const char* dir = getenv("TEMP"); + if (!dir || !*dir) + dir = getenv("TMP"); + if (!dir || !*dir) + dir = "."; +#else + const char* dir = getenv("TMPDIR"); + if (!dir || !*dir) + dir = "/tmp"; +#endif + return FormattableString("%0/glob2-tiled-%1-%2.map").arg(dir).arg(counter++).arg(fileName); + } +} + +namespace MapTiling +{ + bool isActive(int rx, int ry, int teams, int mapTeams) + { + return rx > 1 || ry > 1 || (teams > 0 && teams != mapTeams); + } + + int colonyCount(int mapTeams, int rx, int ry) + { + return mapTeams * rx * ry; + } + + int placedColonyCount(int total, int teams, int perTeam) + { + if (teams < 1) + return 0; + int share = total / teams; + if (perTeam > 0 && perTeam < share) + share = perTeam; + return share * teams; + } + + int teamForColony(int n, int total, int teams, int perTeam) + { + const int placed = placedColonyCount(total, teams, perTeam); + if (n < 0 || n >= total || placed < 1) + return -1; + // colony n is placed when it is the first colony of its stretch of total/placed + for (int i = 0; i < placed; i++) + { + const int at = (i * total) / placed; + if (at == n) + return i % teams; + if (at > n) + break; + } + return -1; + } + + MapInfo readMapInfo(const std::string& fileName) + { + MapInfo info; + std::unique_ptr in(new GAGCore::BinaryInputStream(GAGCore::Toolkit::getFileManager()->openInputStreamBackend(fileName))); + if (in->isEndOfStream() || !info.header.load(in.get()) || !in->canSeek()) + return info; + // the map section starts with its signature and the size shifts, see Map::load + in->seekFromStart(info.header.getMapOffset()); + in->readEnterSection("Map"); + char signature[4]; + in->read(signature, 4, "signatureStart"); + if (memcmp(signature, "MapB", 4) != 0) + return info; + info.w = 1 << in->readSint32("wDec"); + info.h = 1 << in->readSint32("hDec"); + info.valid = info.header.getNumberOfTeams() > 0 && info.w > 0 && info.h > 0; + return info; + } + + bool fits(const MapInfo& map, int rx, int ry, int teams, int swarms) + { + if (!map.valid || map.w * rx > MAX_MAP_SIDE || map.h * ry > MAX_MAP_SIDE) + return false; + const int colonies = colonyCount(map.header.getNumberOfTeams(), rx, ry); + // a count of 0 was not chosen and follows the map, so it never fails + if (teams < 1) + return true; + if (teams > colonies) + return false; + return swarms < 1 || swarms * teams <= colonies; + } + + void adjust(const MapInfo& map, int& rx, int& ry, int& teams, int& swarms) + { + if (!map.valid) + return; + const int mapTeams = map.header.getNumberOfTeams(); + while (map.w * rx > MAX_MAP_SIDE && rx > 1) rx /= 2; + while (map.h * ry > MAX_MAP_SIDE && ry > 1) ry /= 2; + // no colony count chosen yet: one per colony of the repeated map + if (teams < 1) + teams = std::min(Team::MAX_COUNT, colonyCount(mapTeams, rx, ry)); + // grow the repeat, shorter side first, until the colonies wanted fit + while (colonyCount(mapTeams, rx, ry) < teams) + { + const bool canX = map.w * rx * 2 <= MAX_MAP_SIDE, canY = map.h * ry * 2 <= MAX_MAP_SIDE; + if (!canX && !canY) + break; + if ((map.w * rx <= map.h * ry && canX) || !canY) rx *= 2; else ry *= 2; + } + teams = std::max(1, std::min(teams, std::min(Team::MAX_COUNT, colonyCount(mapTeams, rx, ry)))); + if (swarms < 1) + swarms = colonyCount(mapTeams, rx, ry) / teams; + swarms = std::max(1, std::min(swarms, colonyCount(mapTeams, rx, ry) / teams)); + } + + //! Loads the map of `source` into `game` and repeats it; false when either step fails. + static bool loadTiled(const MapHeader& source, int rx, int ry, int teams, int perTeam, Game& game) + { + std::unique_ptr in(new GAGCore::BinaryInputStream(GAGCore::Toolkit::getFileManager()->openInputStreamBackend(source.getFileName()))); + if (in->isEndOfStream()) + return false; + if (!game.load(in.get())) + return false; + return game.tileForPlay(rx, ry, teams, perTeam); + } + + bool tiledThumbnail(const MapHeader& source, int rx, int ry, int teams, int perTeam, MapThumbnail& thumbnail) + { + Game game(NULL, NULL); + if (!loadTiled(source, rx, ry, teams, perTeam, game)) + return false; + thumbnail.loadFromMap(game.map, game.mapHeader); + return true; + } + + MapHeader writeTiledMap(const MapHeader& source, int rx, int ry, int teams, int perTeam) + { + MapHeader failed; + failed.setNumberOfTeams(0); + Game game(NULL, NULL); + if (!loadTiled(source, rx, ry, teams, perTeam, game)) + return failed; + const int placed = placedColonyCount(colonyCount(source.getNumberOfTeams(), rx, ry), teams, perTeam); + std::string name = FormattableString("%0 %1x%2 %3t%4c").arg(source.getMapName()).arg(rx).arg(ry).arg(teams).arg(placed / teams); + MapHeader header = game.mapHeader; + header.setMapName(name); + header.setIsSavedGame(false); + // Tiled maps are generated, not authored: write them to the temp directory + // rather than next to the user's own maps, so they never show up in the + // map list. The file name override makes every later reload of `header` + // (hosting, LAN/YOG transfer) find the same temp file. + const std::string path = tempTiledMapPath(name); + header.setFileNameOverride(path); + std::unique_ptr out(new GAGCore::BinaryOutputStream(GAGCore::Toolkit::getFileManager()->openOutputStreamBackend(path))); + if (out->isEndOfStream()) + return failed; + game.save(out.get(), true, name); + out.reset(); + // read the header back so it carries the checksum of the file as written + std::unique_ptr check(new GAGCore::BinaryInputStream(GAGCore::Toolkit::getFileManager()->openInputStreamBackend(path))); + MapHeader written; + if (check->isEndOfStream() || !written.load(check.get())) + return failed; + written.setMapName(name); + written.setFileNameOverride(path); + return written; + } + + std::vector repeatOptions(int side) + { + std::vector options; + for (int factor = 1; side > 0 && side * factor <= MAX_MAP_SIDE; factor *= 2) + options.push_back(factor); + return options; + } + + MapHeader tiledHeader(const MapHeader& source, int rx, int ry, int teams) + { + MapHeader header = source; + const int mapTeams = source.getNumberOfTeams(); + if (!isActive(rx, ry, teams, mapTeams) || mapTeams < 1) + return header; + if (teams < 1) + teams = std::min(Team::MAX_COUNT, colonyCount(mapTeams, rx, ry)); + if (teams > colonyCount(mapTeams, rx, ry)) + teams = colonyCount(mapTeams, rx, ry); + header.setNumberOfTeams(teams); + for (int k = 0; k < teams; k++) + { + // team k's first colony is colony k, which comes from map team k mod mapTeams + BaseTeam& team = header.getBaseTeam(k); + team = source.getBaseTeam(k % mapTeams); + team.teamNumber = k; + team.numberOfPlayer = 0; + team.playersMask = 0; + float r, g, b; + Utilities::HSVtoRGB(&r, &g, &b, (static_cast(k) * TEAM_COLOR_HUE_DEGREES) / static_cast(teams), Team::TEAM_COLOR_SATURATION, Team::TEAM_COLOR_VALUE); + team.color = GAGCore::Color(static_cast(Team::COLOR_CHANNEL_MAX * r), static_cast(Team::COLOR_CHANNEL_MAX * g), static_cast(Team::COLOR_CHANNEL_MAX * b)); + } + return header; + } +} diff --git a/src/MapTiling.h b/src/MapTiling.h new file mode 100644 index 000000000..b3dcab403 --- /dev/null +++ b/src/MapTiling.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#include "MapHeader.h" + +class MapThumbnail; + +//! Repeating a map in x and y at game setup. The map is copied rx by ry +//! times over the torus, every copy keeps its colonies, and the colonies +//! are dealt round robin to the chosen number of teams; colonies beyond an +//! equal share are dropped. +namespace MapTiling +{ + //! Largest side the engine supports. + static constexpr int MAX_MAP_SIDE = 512; + + //! True when the factors or the team count change the map as loaded. + bool isActive(int rx, int ry, int teams, int mapTeams); + //! Colonies the tiled map contains. + int colonyCount(int mapTeams, int rx, int ry); + //! Colonies actually placed: perTeam for every team, at most an equal share of the total. + int placedColonyCount(int total, int teams, int perTeam); + //! Team that receives colony n, or -1 when it is dropped. The placed colonies are spread evenly over all of them. + int teamForColony(int n, int total, int teams, int perTeam); + //! What the chooser needs to know about a map file without loading it: its header and size in tiles. + struct MapInfo + { + MapHeader header; + int w = 0, h = 0; + bool valid = false; + }; + //! Reads the header and the map size of a map file; valid is false when the file cannot be read. + MapInfo readMapInfo(const std::string& fileName); + //! True when the map can be repeated rx by ry with `teams` colonies of `swarms` bases each; 0 for either means not chosen. + bool fits(const MapInfo& map, int rx, int ry, int teams, int swarms); + //! Adjusts rx, ry, teams and swarms to the map, keeping the colony count whenever any + //! repeat within the size limit reaches it, then the swarms per colony. + void adjust(const MapInfo& map, int& rx, int& ry, int& teams, int& swarms); + //! Writes the repeated map as a map file next to the user's own maps and returns its header, + //! or a header with no teams when the map could not be read or written. + MapHeader writeTiledMap(const MapHeader& source, int rx, int ry, int teams, int perTeam); + //! Renders the preview of the repeated map into `thumbnail`, with buildings and units in the colours + //! of the teams they are dealt to; false when the map could not be read or repeated. + bool tiledThumbnail(const MapHeader& source, int rx, int ry, int teams, int perTeam, MapThumbnail& thumbnail); + //! Repeat factors usable for a side of `side` tiles: powers of two within MAX_MAP_SIDE. + std::vector repeatOptions(int side); + //! The header of the tiled map: `teams` teams whose type follows the + //! colonies round robin, colours spread around the wheel. + MapHeader tiledHeader(const MapHeader& source, int rx, int ry, int teams); +} diff --git a/src/NewMapScreen.cpp b/src/NewMapScreen.cpp index cac1b81ab..f14483277 100644 --- a/src/NewMapScreen.cpp +++ b/src/NewMapScreen.cpp @@ -62,8 +62,9 @@ NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registr addWidget(label); controlWidgets.push_back({c, method, number, toggle, label}); }; - for (const auto &c : GenerationRequest::sharedControls()) + for (const auto &shared : GenerationRequest::sharedControls()) { + const auto c = editorSizeControl(shared); bool size = c.id == "width" || c.id == "height"; int y = c.id == "width" ? 50 : c.id == "height" ? 75 : c.id == "teams" ? 100 : 125; addControl(c, -1, size ? 20 : 310, y); diff --git a/src/SConscript b/src/SConscript index 70214ad89..ffb567f5e 100644 --- a/src/SConscript +++ b/src/SConscript @@ -120,6 +120,7 @@ Game_orders.cpp Game_io.cpp Game_sync.cpp Game_editor.cpp +Game_tiling.cpp render/GameRender.cpp render/GameRenderUnits.cpp render/GameRenderBuildings.cpp @@ -265,6 +266,7 @@ map/gradient/MapGradientField.cpp map/io/MapExploredAreaIO.cpp map/io/MapIO.cpp map/MapMisc.cpp +map/MapTile.cpp map/pathfind/MapPathfindArea.cpp map/pathfind/MapPathfindBuilding.cpp map/pathfind/MapPathfindRessource.cpp @@ -291,6 +293,7 @@ map/edit/MapEditClicks.cpp map/edit/MapEditDialog.cpp map/edit/MapEditKeyActions.cpp map/io/MapHeader.cpp +MapTiling.cpp MapScript.cpp MapScriptError.cpp MapScriptUSL.cpp @@ -774,6 +777,12 @@ if not env['server'] and 'level-gate-test' in COMMAND_LINE_TARGETS: level_gate_sources += local.Object('LevelGateHarness.o', '#test/LevelGateHarness.cpp') local.Alias('level-gate-test', local.Program('LevelGateHarness', level_gate_sources)) +# Repeating a map deals every colony, its areas and flag settings to one team. +if not env['server'] and 'map-tiling-test' in COMMAND_LINE_TARGETS: + tiling_sources = [source for source in source_files if source != 'Glob2.cpp'] + tiling_sources += local.Object('MapTilingHarness.o', '#test/MapTilingHarness.cpp') + local.Alias('map-tiling-test', local.Program('MapTilingHarness', tiling_sources)) + if not env['server'] and 'custom-setup-test' in COMMAND_LINE_TARGETS: custom_sources = [source for source in source_files if source != 'Glob2.cpp'] custom_sources += local.Object('CustomGameSetupHarness.o', '#test/CustomGameSetupHarness.cpp') diff --git a/src/building/Building.h b/src/building/Building.h index d8e7b3b84..cff321c37 100644 --- a/src/building/Building.h +++ b/src/building/Building.h @@ -653,6 +653,9 @@ class Building : public BuildingUtils // units: scratch counters for subscription / priority diff Sint32 maxUnitWorkingFuture; +public: + Sint32 getMaxUnitWorkingFuture() const { return maxUnitWorkingFuture; } +private: Sint32 maxUnitWorkingPrevious; ///The subscribeToBringResourcesStep and subscribeForFlagingStep operate every 32 ticks Sint32 subscriptionWorkingTimer; diff --git a/src/map/Map.h b/src/map/Map.h index 280fba960..e053f6ea0 100644 --- a/src/map/Map.h +++ b/src/map/Map.h @@ -108,6 +108,8 @@ class Map //! Reset map size to width = 2^wDec and height=2^hDec, and fill background with terrainType void setSize(int wDec, int hDec, TerrainType terrainType=WATER); + //! Repeat the terrain, resources and discovery rx by ry times; units, buildings and team zones are dropped + void tile(int rx, int ry); // !This call is needed to use the Map! void setGame(Game *game); //! Load a map from a stream and relink with associated game diff --git a/src/map/MapTile.cpp b/src/map/MapTile.cpp new file mode 100644 index 000000000..fb63345ca --- /dev/null +++ b/src/map/MapTile.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Repeating the map arrays over the torus at game setup: see MapTiling.h. +#include "Map.h" + +#include +#include +#include + +void Map::tile(int rx, int ry) +{ + assert(arraysBuilt); + int addWDec = 0, addHDec = 0; + while ((1 << addWDec) < rx) addWDec++; + while ((1 << addHDec) < ry) addHDec++; + assert((1 << addWDec) == rx && (1 << addHDec) == ry); + + const int oldW = w, oldH = h, oldWDec = wDec; + const std::vector oldTiles = tiles; + const std::vector oldDiscovered = mapDiscovered; + const std::vector oldUndermap(undermap, undermap + size); + std::string names[9]; + for (int n = 0; n < 9; n++) + names[n] = getAreaName(n); + + setSize(oldWDec + addWDec, hDec + addHDec); + // setSize marks every cell as held by an immobile unit, which the + // building gradients read as an obstacle; a loaded map has none + memset(immobileUnits, 255, size); + + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + const size_t src = ((y % oldH) << oldWDec) + (x % oldW); + const size_t dst = coordToIndex(x, y); + // units and buildings are placed again per colony, and the + // per-team zones belong to teams that are rebuilt + tiles[dst] = oldTiles[src]; + tiles[dst].building = NOGBID; + tiles[dst].groundUnit = NOGUID; + tiles[dst].airUnit = NOGUID; + tiles[dst].forbidden = 0; + tiles[dst].guardArea = 0; + tiles[dst].clearArea = 0; + mapDiscovered[dst] = oldDiscovered[src]; + undermap[dst] = oldUndermap[src]; + } + for (int n = 0; n < 9; n++) + setAreaName(n, names[n]); +} diff --git a/src/map/generator/core/GenerationValidation.cpp b/src/map/generator/core/GenerationValidation.cpp index 8087fd4e9..e3430a87b 100644 --- a/src/map/generator/core/GenerationValidation.cpp +++ b/src/map/generator/core/GenerationValidation.cpp @@ -9,9 +9,13 @@ #include "Unit.h" std::string validateGenerationRequest(const GenerationRequest &r, const GeneratorDefinition &d) { - for (const auto &c : sharedGeneratorControls()) + // A request is valid at any size either screen offers: the editor's range includes the lobby's. + for (const auto &shared : sharedGeneratorControls()) + { + const GeneratorControl c = editorSizeControl(shared); if (c.get(r) != c.normalize(c.get(r))) return "Invalid " + c.id; + } if (r.terrainType < WATER || r.terrainType > GRASS) return "Invalid terrain"; for (const auto &c : d.controls) diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp index 7a73b04b4..cccbd4146 100644 --- a/src/map/generator/core/GeneratorControls.cpp +++ b/src/map/generator/core/GeneratorControls.cpp @@ -96,6 +96,13 @@ void GeneratorControl::set(GenerationRequest &r, int value) const else r.options[id] = v; } +GeneratorControl editorSizeControl(const GeneratorControl &shared) +{ + GeneratorControl c = shared; + if (c.id == "width" || c.id == "height") + c.minimum = EDITOR_MINIMUM_SIZE_SHIFT; + return c; +} const std::vector &sharedGeneratorControls() { static const std::vector controls = { @@ -194,8 +201,9 @@ void GenerationHistory::select(GenerationRequest ¤t, int id, next = it->second; else next.setMethodDefaults(id, registry); + // carry the size as is: an editor map of 32 stays 32 when the landscape changes for (const auto &c : sharedGeneratorControls()) - c.set(next, c.get(current)); + editorSizeControl(c).set(next, c.get(current)); next.terrainType = current.terrainType; next.seed = current.seed; current = next; diff --git a/src/map/generator/core/GeneratorControls.h b/src/map/generator/core/GeneratorControls.h index b94ca6f30..9465c0b94 100644 --- a/src/map/generator/core/GeneratorControls.h +++ b/src/map/generator/core/GeneratorControls.h @@ -58,3 +58,8 @@ struct GeneratorControl void set(MapGenerationDescriptor &, int) const; }; const std::vector &sharedGeneratorControls(); +//! The editor also offers 32 x 32 maps, the natural source for repeating a map (MapTiling). +//! The lobby keeps 64 as its smallest size; many landscapes cannot seat colonies on less. +static constexpr int EDITOR_MINIMUM_SIZE_SHIFT = 5; +//! `shared` with the editor's smaller minimum when it is the width or height control. +GeneratorControl editorSizeControl(const GeneratorControl &shared); diff --git a/src/map/io/MapHeader.cpp b/src/map/io/MapHeader.cpp index 7f6e386a0..1eb928bc3 100644 --- a/src/map/io/MapHeader.cpp +++ b/src/map/io/MapHeader.cpp @@ -22,6 +22,7 @@ void MapHeader::reset() mapName = ""; mapOffset = 0; isSavedGame=false; + fileNameOverride = ""; resetGameSHA1(); } @@ -137,6 +138,8 @@ const std::string& MapHeader::getMapName() const std::string MapHeader::getFileName(bool isCampaignMap, bool isReplay) const { + if (!fileNameOverride.empty()) + return fileNameOverride; if(isReplay) return glob2NameToFilename("replays", mapName, "replay"); else if(isCampaignMap) @@ -156,6 +159,13 @@ void MapHeader::setMapName(const std::string& newMapName) +void MapHeader::setFileNameOverride(const std::string& fileName) +{ + fileNameOverride = fileName; +} + + + Uint32 MapHeader::getMapOffset() const { return mapOffset; diff --git a/src/map/io/MapHeader.h b/src/map/io/MapHeader.h index 5afa32422..27c1b9e81 100644 --- a/src/map/io/MapHeader.h +++ b/src/map/io/MapHeader.h @@ -46,12 +46,19 @@ class MapHeader /// Returns the user-friendly name of the map const std::string& getMapName() const; - /// Returns the file name of the map. isCampaignMap is a special override when the game is loading campaign maps + /// Returns the file name of the map. isCampaignMap is a special override when the game is loading campaign maps. + /// If a file name override is set, it is returned as-is and isCampaignMap/isReplay are ignored. std::string getFileName(bool isCampaignMap=false, bool isReplay=false) const; - + /// Sets the user-friendly name of the map void setMapName(const std::string& newMapName); + /// Makes getFileName() return `fileName` verbatim instead of deriving it from mapName. + /// For maps generated to a location outside the usual maps/games/replays/campaigns + /// directories (e.g. MapTiling's temporary files) so every later reload of this same + /// MapHeader finds the file without listing it among the user's own maps. + void setFileNameOverride(const std::string& fileName); + /// Returns the offset of the Map info in the game save. /// This is used to quickly gain access to Map data, for /// the generation of map previews @@ -113,6 +120,9 @@ class MapHeader Uint8 SHA1[20]; std::string mapName; + + /// See setFileNameOverride(). Empty unless explicitly set; never serialized. + std::string fileNameOverride; }; diff --git a/src/map/io/MapThumbnail.cpp b/src/map/io/MapThumbnail.cpp index e54a6bc6d..50a7cf288 100644 --- a/src/map/io/MapThumbnail.cpp +++ b/src/map/io/MapThumbnail.cpp @@ -5,6 +5,8 @@ #include "GUIBase.h" #include "Map.h" #include "MapHeader.h" +#include "Building.h" +#include "Unit.h" #include "Toolkit.h" #include #include @@ -142,6 +144,16 @@ void MapThumbnail::loadFromMap(const std::string &filename) } void MapThumbnail::loadFromMap(const Map &map) +{ + render(map, nullptr); +} + +void MapThumbnail::loadFromMap(const Map &map, const MapHeader &header) +{ + render(map, &header); +} + +void MapThumbnail::render(const Map &map, const MapHeader *header) { *this = MapThumbnail(); const int mw = map.getW(), mh = map.getH(); @@ -155,7 +167,7 @@ void MapThumbnail::loadFromMap(const Map &map) for (int y = 0; y < result->height; ++y) for (int x = 0; x < result->width; ++x) { - int sums[3] = {}, count = 0; + int sums[3] = {}, count = 0, team = -1; // Half-open cells neither double-count boundaries nor sample the next // torus period at the right/bottom edge. for (int sy = y * mh / result->height; sy < (y + 1) * mh / result->height; ++sy) @@ -174,9 +186,19 @@ void MapThumbnail::loadFromMap(const Map &map) for (int c = 0; c < 3; ++c) sums[c] += colors[color][c]; ++count; + // a building or unit paints the whole pixel in its team's colour + if (header && map.getBuilding(sx, sy) != NOGBID) + team = Building::GIDtoTeam(map.getBuilding(sx, sy)); + else if (header && team < 0 && map.getGroundUnit(sx, sy) != NOGUID) + team = Unit::GIDtoTeam(map.getGroundUnit(sx, sy)); } + const bool teamPixel = header && team >= 0 && team < header->getNumberOfTeams(); for (int c = 0; c < 3; ++c) - result->rgb[(y * result->width + x) * 3 + c] = sums[c] / count; + { + const auto &teamColor = teamPixel ? header->getBaseTeam(team).color : GAGCore::Color(); + const int teamChannel = c == 0 ? teamColor.r : c == 1 ? teamColor.g : teamColor.b; + result->rgb[(y * result->width + x) * 3 + c] = teamPixel ? teamChannel : sums[c] / count; + } } lastW = mw; lastH = mh; diff --git a/src/map/io/MapThumbnail.h b/src/map/io/MapThumbnail.h index b2ec0f918..53afb5513 100644 --- a/src/map/io/MapThumbnail.h +++ b/src/map/io/MapThumbnail.h @@ -12,6 +12,7 @@ class OutputStream; class InputStream; } // namespace GAGCore class Map; +class MapHeader; // Immutable, shared terrain pixels. The legacy wire image remains 128x128; // local previews retain up to 512 pixels along the longest map dimension. @@ -27,6 +28,8 @@ class MapThumbnail }; void loadFromMap(const std::string &filename); void loadFromMap(const Map &map); + // Buildings and units take the colour of their team in header, as on the repeated-map preview. + void loadFromMap(const Map &map, const MapHeader &header); void encodeData(GAGCore::OutputStream *stream) const; void decodeData(GAGCore::InputStream *stream, Uint32 versionMinor); void loadIntoSurface(GAGCore::DrawableSurface *surface) const; @@ -36,6 +39,7 @@ class MapThumbnail const std::shared_ptr &pixels() const { return image; } private: + void render(const Map &map, const MapHeader *header); std::shared_ptr image; int lastW = 0, lastH = 0; }; diff --git a/test/LANSessionHarness.cpp b/test/LANSessionHarness.cpp index 00ccc056a..6b81c7437 100644 --- a/test/LANSessionHarness.cpp +++ b/test/LANSessionHarness.cpp @@ -8,6 +8,7 @@ #include "YOGServer.h" #include "FileManager.h" #include "GUITextInput.h" +#include "MapTiling.h" #include "Toolkit.h" #include @@ -51,7 +52,9 @@ Uint32 timeoutTimer(Uint32, void*) class JoinScreen : public LANFindScreen { public: - JoinScreen(const std::string& address, const std::string& capture) : capture(capture) + // A tiled map is a 256 x 256 map of about 3 MB, which takes the transfer most of a minute. + JoinScreen(const std::string& address, const std::string& capture, bool tiled) + : capture(capture), stay(tiled ? 80000 : 25000) { // Use the real form's public widget API; no networking is stubbed. for (Widget* widget : widgets) @@ -65,19 +68,20 @@ class JoinScreen : public LANFindScreen // SDL timers only enqueue input. Rendering and network state stay on // the real screen's main thread, including the nested lobby loop. SDL_TimerID ready = SDL_AddTimer(5000, readyTimer, nullptr); - SDL_TimerID leave = SDL_AddTimer(25000, leaveTimer, nullptr); - SDL_TimerID timeout = SDL_AddTimer(40000, timeoutTimer, nullptr); + SDL_TimerID leave = SDL_AddTimer(stay, leaveTimer, nullptr); + SDL_TimerID timeout = SDL_AddTimer(stay + 15000, timeoutTimer, nullptr); LANFindScreen::onAction(nullptr, BUTTON_RELEASED, CONNECT, 0); SDL_RemoveTimer(ready); SDL_RemoveTimer(leave); SDL_RemoveTimer(timeout); SDL_SaveBMP(globalContainer->gfx->getSDLSurface(), capture.c_str()); - bool ok = returnCode != QUIT_APPLICATION && SDL_GetTicks64() - start < 39000; + bool ok = returnCode != QUIT_APPLICATION && SDL_GetTicks64() - start < stay + 14000; std::puts(ok ? "JOIN PASS: lobby returned through Leave Game" : "JOIN FAIL: lobby did not complete before the timeout"); endExecute(ok ? 0 : 1); } private: std::string capture; + Uint32 stay; }; class HostScreen : public Glob2TabScreen @@ -153,7 +157,18 @@ class HostScreen : public Glob2TabScreen Uint64 start; }; -int host(int cycles, const std::string& capture) +// With `tiled`, the host offers FourSquares1 repeated 2 x 2 for four colonies instead. That map +// exists only in the temp directory, behind MapHeader's file name override, and never in the +// maps folder, so the guest can only get it by transfer; the host leaves a copy of the exact +// bytes next to the captures for the guest to compare. +const char* TILED_DOWNLOAD = "maps/FourSquares1_2x2_4t1c.map"; + +std::filesystem::path tiledSourceCopy(const std::string& capture) +{ + return std::filesystem::path(capture).parent_path() / "tiled-source.map"; +} + +int host(int cycles, const std::string& capture, bool tiled) { auto client = std::make_shared(); auto server = std::make_shared(YOGAnonymousLogin, YOGSingleGame); @@ -167,12 +182,23 @@ int host(int cycles, const std::string& capture) auto game = std::make_shared(client); client->setMultiplayerGame(game); game->createNewGame("LAN regression"); - // A private map name forces a real transfer without touching user maps. MapHeader map = Engine::loadMapHeader("maps/FourSquares1.map"); - map.setMapName("LAN regression transfer"); - std::filesystem::copy_file("maps/FourSquares1.map", - Toolkit::getFileManager()->getDir(0) + "/" + map.getFileName(), - std::filesystem::copy_options::overwrite_existing); + if (tiled) + { + map = MapTiling::writeTiledMap(map, 2, 2, 4, 1); + if (map.getNumberOfTeams() != 4) { std::puts("HOST FAIL: could not write the tiled map"); return 1; } + std::filesystem::copy_file(map.getFileName(), tiledSourceCopy(capture), + std::filesystem::copy_options::overwrite_existing); + std::printf("HOST tiled map %s at %s\n", map.getMapName().c_str(), map.getFileName().c_str()); + } + else + { + // A private map name forces a real transfer without touching user maps. + map.setMapName("LAN regression transfer"); + std::filesystem::copy_file("maps/FourSquares1.map", + Toolkit::getFileManager()->getDir(0) + "/" + map.getFileName(), + std::filesystem::copy_options::overwrite_existing); + } game->setMapHeader(map); HostScreen screen(game, cycles, capture); MultiplayerGameScreen lobby(&screen, game, client); @@ -184,11 +210,12 @@ int host(int cycles, const std::string& capture) int main(int argc, char** argv) { - if (argc != 5) + if (argc != 5 && !(argc == 6 && std::string(argv[5]) == "tiled")) { - std::fprintf(stderr, "Usage: %s host|join address cycles capture-prefix\n", argv[0]); + std::fprintf(stderr, "Usage: %s host|join address cycles capture-prefix [tiled]\n", argv[0]); return 2; } + const bool tiled = argc == 6; std::setvbuf(stdout, nullptr, _IONBF, 0); SDL_setenv("SDL_AUDIODRIVER", "dummy", 0); GlobalContainer globals; @@ -210,16 +237,17 @@ int main(int argc, char** argv) globals.load(); if (SDLNet_Init() < 0) return 1; int rc = 0; - if (std::string(argv[1]) == "host") rc = host(std::stoi(argv[3]), argv[4]); + if (std::string(argv[1]) == "host") rc = host(std::stoi(argv[3]), argv[4], tiled); else for (int cycle = 0; cycle < std::stoi(argv[3]) && !rc; ++cycle) { - const auto downloaded = std::filesystem::path(globals.fileManager->getDir(0)) / "maps/LAN_regression_transfer.map"; + const auto downloaded = std::filesystem::path(globals.fileManager->getDir(0)) / + (tiled ? TILED_DOWNLOAD : "maps/LAN_regression_transfer.map"); // Force a second request too: rejoining must reuse the server's upload // rather than append another copy of its chunks to the cached transfer. std::filesystem::remove(downloaded); - JoinScreen screen(argv[2], std::string(argv[4]) + "-" + std::to_string(cycle + 1) + ".bmp"); + JoinScreen screen(argv[2], std::string(argv[4]) + "-" + std::to_string(cycle + 1) + ".bmp", tiled); rc = screen.execute(globals.gfx, 20); - std::ifstream original("maps/FourSquares1.map", std::ios::binary); + std::ifstream original(tiled ? tiledSourceCopy(argv[4]) : std::filesystem::path("maps/FourSquares1.map"), std::ios::binary); std::ifstream received(downloaded, std::ios::binary); std::string expected((std::istreambuf_iterator(original)), {}); std::string actual((std::istreambuf_iterator(received)), {}); diff --git a/test/MapTilingHarness.cpp b/test/MapTilingHarness.cpp new file mode 100644 index 000000000..961f4c291 --- /dev/null +++ b/test/MapTilingHarness.cpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Real-engine regression for repeating a map (Game::tileForPlay): every copy of every colony +// lands on the team MapTiling deals it to, with its buildings, units, painted forbidden, guard +// and clearing areas and its clearing flags' resource choices. +#include "GlobalContainer.h" +#include "BinaryStream.h" +#include "Building.h" +#include "BuildingType.h" +#include "FileManager.h" +#include "Game.h" +#include "GenerationRequest.h" +#include "GenerationService.h" +#include "GeneratorControls.h" +#include "MapTiling.h" +#include "Team.h" +#include "Toolkit.h" +#include "Unit.h" +#include +#include +#include +#include +#include + +GlobalContainer* globalContainer = nullptr; + +namespace +{ +void require(bool ok, const char* message) +{ + if (!ok) + { + std::fprintf(stderr, "FAIL: %s\n", message); + std::exit(1); + } +} + +bool load(Game& game, const char* file) +{ + std::unique_ptr in(new GAGCore::BinaryInputStream(GAGCore::Toolkit::getFileManager()->openInputStreamBackend(file))); + return !in->isEndOfStream() && game.load(in.get()); +} + +int buildingsOf(const Team* team) +{ + int n = 0; + for (int i = 0; i < Building::MAX_COUNT; i++) + n += team->myBuildings[i] && team->myBuildings[i]->buildingState == Building::ALIVE; + return n; +} + +int unitsOf(const Team* team) +{ + int n = 0; + for (int i = 0; i < Unit::MAX_COUNT; i++) + n += team->myUnits[i] && !team->myUnits[i]->isDead; + return n; +} + +// The team bits of one kind of area on a tile. +Uint32 areaMask(const Map& map, int x, int y, int kind) +{ + Uint32 mask = 0; + for (int t = 0; t < Team::MAX_COUNT; t++) + { + const Uint32 bit = Team::teamNumberToMask(t); + const bool on = kind == 0 ? map.isForbidden(x, y, bit) : kind == 1 ? map.isGuardArea(x, y, bit) : map.isClearArea(x, y, bit); + if (on) + mask |= bit; + } + return mask; +} +} + +int main(int argc, char** argv) +{ + SDL_SetMainReady(); + require(argc == 2 && std::string(argv[1]).find("glob2-save-test-") == 0, "usage: MapTilingHarness "); + GlobalContainer globals(argv[1]); + globalContainer = &globals; + globals.runNoX = true; + globals.load(); + + Game game(nullptr, nullptr); + require(load(game, "maps/balanced_for_2.map"), "load balanced_for_2"); + const int w0 = game.map.getW(), h0 = game.map.getH(), mapTeams = game.mapHeader.getNumberOfTeams(); + require(mapTeams == 2, "balanced_for_2 has two colonies"); + + // Paint an area of each kind for team 0 and a forbidden tile for team 1, and give team 0 a + // clearing flag that leaves wood alone. + struct Paint { int x, y, team, kind; }; + const Paint paints[] = {{3, 5, 0, 0}, {4, 5, 0, 1}, {5, 5, 0, 2}, {10, 20, 1, 0}}; + for (const Paint& p : paints) + { + if (p.kind == 0) + game.map.addForbidden(p.x, p.y, p.team); + else if (p.kind == 1) + game.map.addGuardArea(p.x, p.y, p.team); + else + game.map.addClearArea(p.x, p.y, p.team); + } + const int flagType = globals.buildingsTypes.getTypeNum("clearingflag", 0, false); + require(flagType >= 0, "clearing flag type exists"); + Building* flag = game.addBuilding(20, 40, flagType, 0); + require(flag != nullptr, "place a clearing flag"); + flag->clearingResources[WOOD] = false; + + int buildings[2], units[2]; + for (int t = 0; t < mapTeams; t++) + { + buildings[t] = buildingsOf(game.teams[t]); + units[t] = unitsOf(game.teams[t]); + } + + const int rx = 2, ry = 2, teams = 8; + const int total = MapTiling::colonyCount(mapTeams, rx, ry); + require(total == 8, "2 x 2 holds eight colonies"); + require(game.tileForPlay(rx, ry, teams, 1), "tileForPlay succeeds"); + require(game.map.getW() == w0 * rx && game.map.getH() == h0 * ry, "the map is repeated 2 x 2"); + require(game.mapHeader.getNumberOfTeams() == teams, "one team per colony"); + + int n = 0; + for (int j = 0; j < ry; j++) + for (int i = 0; i < rx; i++) + for (int t = 0; t < mapTeams; t++, n++) + { + const int k = MapTiling::teamForColony(n, total, teams, 1); + require(k >= 0 && k < teams, "every colony is kept"); + require(buildingsOf(game.teams[k]) == buildings[t], "the copy keeps its colony's buildings"); + require(unitsOf(game.teams[k]) == units[t], "the copy keeps its colony's units"); + for (const Paint& p : paints) + if (p.team == t) + require(areaMask(game.map, p.x + i * w0, p.y + j * h0, p.kind) == Team::teamNumberToMask(k), + "a painted area belongs to exactly the copy's team"); + if (t == 0) + { + int flags = 0; + for (int b = 0; b < Building::MAX_COUNT; b++) + { + const Building* copy = game.teams[k]->myBuildings[b]; + if (copy && copy->typeNum == flagType) + { + flags++; + require(!copy->clearingResources[WOOD] && copy->clearingResources[WHEAT], "the clearing flag keeps its choice"); + } + } + require(flags == 1, "the copy has its clearing flag"); + } + } + std::puts("PASS a repeated map deals each colony, its areas and its flag settings to one team"); + + // The source of a repeat is often a 32 x 32 map: the editor offers that size, the lobby keeps 64. + for (const auto& shared : GenerationRequest::sharedControls()) + if (shared.id == "width" || shared.id == "height") + { + require(shared.minimum == 6, "the lobby's smallest map is 64 tiles"); + require(editorSizeControl(shared).minimum == 5, "the editor's smallest map is 32 tiles"); + } + GenerationRequest request; + request.setMethodDefaults(GenerationRequest::eUNIFORM); + request.wDec = request.hDec = 5; + request.seed = 1; + Game small(nullptr, nullptr); + require(bool(GenerationService().generate(small, request)), "a 32 x 32 map generates"); + require(small.map.getW() == 32 && small.map.getH() == 32, "the generated map is 32 x 32"); + std::puts("PASS the editor can make the 32 x 32 map a repeat starts from"); + return 0; +} diff --git a/test/README.md b/test/README.md index b5b10fd6c..44b9bd9eb 100644 --- a/test/README.md +++ b/test/README.md @@ -100,6 +100,21 @@ To poke `cases[i].terrain` directly (`regenerateMap` is protected): grass < 16, - Adding regression tests around any Map state mutator before refactoring it. - **Don't use** for behaviors that genuinely need real `Game` / `Team` / `Unit` / `Building` wiring (e.g. `doesUnitTouchEnemy` reaches into `game->teams[]->myBuildings[]`) — those need either a different stub set or a refactor to decouple first. +## Map tiling regression + +```sh +scons -j2 release=1 server=0 map-tiling-test +python3 test/run-savegame-safety-tests.py build/src/MapTilingHarness +``` + +Loads `maps/balanced_for_2.map`, paints a forbidden, guard and clearing area for one colony +and a forbidden tile for the other, adds a clearing flag that leaves wood alone, then repeats +the map 2 x 2 for eight teams with `Game::tileForPlay`. Each copy's team, as +`MapTiling::teamForColony` deals it, must hold its colony's buildings and units, exactly its +own bit on every painted tile, and a clearing flag with the same resource choice. +It also checks that the editor's width and height reach 32 while the lobby's stop at 64, and +that a 32 x 32 map generates. + ## Real LAN session regression From the repository root: @@ -117,6 +132,14 @@ source (`maps/FourSquares1.map`). The host verifies readiness, roster size, uniq player IDs, slot masks, and both departures. The map's current size is not hardcoded in the test. Linux CI runs this automatically with SDL's dummy video/audio drivers. +With `--tiled` (and `tiled` as a fifth argument to the harness), the host instead offers +`FourSquares1` repeated 2 x 2 for four colonies. `MapTiling::writeTiledMap` puts that map in +the temp directory behind `MapHeader`'s file name override, never in a maps folder, so the +guest can only get it by transfer; the guest compares its download, +`maps/FourSquares1_2x2_4t1c.map`, with the copy the host leaves as `tiled-source.map` in the +output directory. The 256 x 256 map is about 3 MB, so each cycle gives the transfer 80 seconds +before leaving. CI runs both modes. + For two physical machines, run these from each machine's repository root, using absolute capture prefixes whose parent directories already exist: diff --git a/test/run_lan_session_test.py b/test/run_lan_session_test.py index 02803926c..a833fa221 100644 --- a/test/run_lan_session_test.py +++ b/test/run_lan_session_test.py @@ -11,6 +11,8 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("binary", type=Path) parser.add_argument("--output", type=Path, default=Path("output/lan-session-test")) + parser.add_argument("--tiled", action="store_true", + help="host a map repeated 2 x 2 that lives only in the temp directory") args = parser.parse_args() binary = args.binary.resolve() output = args.output.resolve() @@ -18,11 +20,12 @@ def main(): env = os.environ.copy() env.setdefault("SDL_VIDEODRIVER", "dummy") env.setdefault("SDL_AUDIODRIVER", "dummy") + extra = ["tiled"] if args.tiled else [] host_log = output / "host.log" join_log = output / "join.log" with host_log.open("w") as host_out, join_log.open("w") as join_out: host = subprocess.Popen( - [str(binary), "host", "127.0.0.1", "2", str(output / "host")], + [str(binary), "host", "127.0.0.1", "2", str(output / "host")] + extra, stdout=host_out, stderr=subprocess.STDOUT, env=env, ) try: @@ -32,8 +35,8 @@ def main(): raise RuntimeError("Host did not publish a lobby") time.sleep(0.1) joined = subprocess.run( - [str(binary), "join", "127.0.0.1", "2", str(output / "join")], - stdout=join_out, stderr=subprocess.STDOUT, env=env, timeout=100, + [str(binary), "join", "127.0.0.1", "2", str(output / "join")] + extra, + stdout=join_out, stderr=subprocess.STDOUT, env=env, timeout=250 if args.tiled else 100, ) if joined.returncode != 0: raise RuntimeError(f"Join/transfer/leave test exited {joined.returncode}") @@ -54,7 +57,8 @@ def main(): host.wait() print(host_log.read_text()) print(join_log.read_text()) - print("LAN session regression PASS: two ready/join/leave cycles and exact map downloads") + kind = "tiled map" if args.tiled else "map" + print(f"LAN session regression PASS: two ready/join/leave cycles and exact {kind} downloads") return 0 diff --git a/test/translation_shared_values.json b/test/translation_shared_values.json index 03a040226..d4167aeb3 100644 --- a/test/translation_shared_values.json +++ b/test/translation_shared_values.json @@ -64,7 +64,8 @@ "Hospital", "Magenta", "Total", - "Tutorial" + "Tutorial", + "bases" ], "texts.ca.txt": [ "Audio", @@ -75,7 +76,8 @@ "No", "Total", "Tutorial", - "desert" + "desert", + "bases" ], "texts.cz.txt": [ "Audio", @@ -138,7 +140,8 @@ "No", "Plazas", "Total", - "Tutorial" + "Tutorial", + "bases" ], "texts.eu.txt": [ "Info", @@ -168,7 +171,8 @@ "Total", "Version", "colonies", - "options" + "options", + "bases" ], "texts.gr.txt": [], "texts.hu.txt": [ @@ -211,7 +215,8 @@ "Team", "Tutorial", "Water", - "element" + "element", + "bases" ], "texts.pl.txt": [ "Audio", @@ -224,7 +229,8 @@ "Hospital", "Magenta", "Total", - "Tutorial" + "Tutorial", + "bases" ], "texts.ro.txt": [ "Audio",