From 08c271aadeb7b81916d2cc940396a0ad853db75c Mon Sep 17 00:00:00 2001 From: Daniel <101683475+Koranir@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:41:03 +1000 Subject: [PATCH 01/75] fix(mechanics): Don't pass through wormholes before escorts are ready (#10114) --- source/AI.cpp | 22 ++++++++++++++++++++++ source/Ship.cpp | 3 +++ 2 files changed, 25 insertions(+) diff --git a/source/AI.cpp b/source/AI.cpp index 2ce210324027..8850f9c74817 100644 --- a/source/AI.cpp +++ b/source/AI.cpp @@ -117,6 +117,25 @@ namespace { return true; } + bool EscortsReadyToLand(const Ship &ship) + { + bool shipIsYours = ship.IsYours(); + const Government *gov = ship.GetGovernment(); + for(const weak_ptr &ptr : ship.GetEscorts()) + { + shared_ptr escort = ptr.lock(); + // Skip escorts which are not player-owned and not escort mission NPCs. + if(!escort || (shipIsYours && !escort->IsYours() && (!escort->GetPersonality().IsEscort() + || gov->IsEnemy(escort->GetGovernment())))) + continue; + if(escort->IsDisabled()) + continue; + if(escort->GetTargetStellar() == ship.GetTargetStellar() && !escort->CanLand()) + return false; + } + return true; + } + // Determine if the ship has any usable weapons. bool IsArmed(const Ship &ship) { @@ -4405,6 +4424,9 @@ void AI::MovePlayer(Ship &ship, Command &activeCommands) if(autoPilot.Has(Command::LAND) || (autoPilot.Has(Command::JUMP | Command::FLEET_JUMP) && isWormhole)) { + if(activeCommands.Has(Command::WAIT) || (autoPilot.Has(Command::FLEET_JUMP) && !EscortsReadyToLand(ship))) + command |= Command::WAIT; + if(ship.GetPlanet()) autoPilot.Clear(Command::LAND | Command::JUMP | Command::FLEET_JUMP); else diff --git a/source/Ship.cpp b/source/Ship.cpp index 1c20d1d904a3..e70b538caa64 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -2202,6 +2202,9 @@ bool Ship::CanLand() const if(!GetTargetStellar()->GetPlanet()->CanLand(*this)) return false; + if(commands.Has(Command::WAIT)) + return false; + Point distance = GetTargetStellar()->Position() - position; double speed = velocity.Length(); From df15c3396b38c9faa85f193c319b76e3f9049b1a Mon Sep 17 00:00:00 2001 From: Nicholas Shanks Date: Wed, 5 Jun 2024 07:52:48 +0100 Subject: [PATCH 02/75] feat(ui): Provide more user settings for scrolling speed (#10150) --- source/PreferencesPanel.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/PreferencesPanel.cpp b/source/PreferencesPanel.cpp index 15ce905483a7..074caa0e80e8 100644 --- a/source/PreferencesPanel.cpp +++ b/source/PreferencesPanel.cpp @@ -357,9 +357,9 @@ bool PreferencesPanel::Scroll(double dx, double dy) { int speed = Preferences::ScrollSpeed(); if(dy < 0.) - speed = max(20, speed - 20); + speed = max(10, speed - 10); else - speed = min(60, speed + 20); + speed = min(60, speed + 10); Preferences::SetScrollSpeed(speed); } return true; @@ -1231,10 +1231,10 @@ void PreferencesPanel::HandleSettingsString(const string &str, Point cursorPosit } else if(str == SCROLL_SPEED) { - // Toggle between three different speeds. - int speed = Preferences::ScrollSpeed() + 20; + // Toggle between six different speeds. + int speed = Preferences::ScrollSpeed() + 10; if(speed > 60) - speed = 20; + speed = 10; Preferences::SetScrollSpeed(speed); } else if(str == DATE_FORMAT) From 0451d9081b2fafdb32aa22734d3a553a4826c9f7 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:48:27 +0200 Subject: [PATCH 03/75] fix(typo): `carring` -> `carrying` in coalition intro (#10169) --- data/coalition/lunarium intro.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/coalition/lunarium intro.txt b/data/coalition/lunarium intro.txt index 429284170c7c..8b857c84b58d 100644 --- a/data/coalition/lunarium intro.txt +++ b/data/coalition/lunarium intro.txt @@ -961,7 +961,7 @@ mission "Lunarium: Evacuation 1" ` "Sorry, I'm not available at the moment."` decline label alright - ` One of them brings you to the Spire, sending a message as you two head there, and the hatch is opened right as you arrive. They thank you for helping, and rush to board their own ship and depart. You go up the Spire's ramp and meet some more Kimek from the company, who greet you and guide you through the ship. Halfway through a corridor, they stop and ask that you wait there. A different Kimek carring nearly a dozen different devices emerges from a side corridor, and the group begins patting and circling your body with the devices. You ask them what they're doing, but they don't respond. After every single one of the machines has finished its beeps and chirps around you, and the Kimek seem satisfied, they continue on. Surprisingly, they bring you not to the cockpit, but to one of the bunks, where a Kimek is talking with a Saryd man. Once she sees you're there, the Kimek gives a glance to the Saryd, and leaves with the others, leaving you two alone.` + ` One of them brings you to the Spire, sending a message as you two head there, and the hatch is opened right as you arrive. They thank you for helping, and rush to board their own ship and depart. You go up the Spire's ramp and meet some more Kimek from the company, who greet you and guide you through the ship. Halfway through a corridor, they stop and ask that you wait there. A different Kimek carrying nearly a dozen different devices emerges from a side corridor, and the group begins patting and circling your body with the devices. You ask them what they're doing, but they don't respond. After every single one of the machines has finished its beeps and chirps around you, and the Kimek seem satisfied, they continue on. Surprisingly, they bring you not to the cockpit, but to one of the bunks, where a Kimek is talking with a Saryd man. Once she sees you're there, the Kimek gives a glance to the Saryd, and leaves with the others, leaving you two alone.` branch lunarium has "joined the lunarium" ` Middle-aged, the Saryd has little hair on his head and a dense beard. "Greetings, human friend. your name is, correct?" You nod. "Apologize I do, for the safety measures experienced, you must have. You see, rather sickly lately, I have been, and while usually fine around Kimek or Arach employees and passengers, wish to try my health against a Saryd or human disease, I did not. But, come to listen of my woes, you have not. Called Elirom, and owner of this company, I am. For those wishing to travel in leisure or for work, provide comfortable ships we do," he explains. "Run into some trouble, one of our ships has. Supposed to transport students and teachers back to Second Viridian after their school trip was done, but now waiting for repairs for the ship's malfunctioning outfits, they are. Picked up by , the students must be, so as to not miss their tests, but elsewhere busy, our other ships are. Pick them up in our stead, would you? from our company, you will receive."` From 1b4dbe3ab44756b01f707e81fdde7ef965cd4f90 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:07:09 +0300 Subject: [PATCH 04/75] fix(content): Switch all instances of sizeable to sizable (#10171) --- data/bunrodea/bunrodea weapons.txt | 2 +- data/human/free worlds 1 start.txt | 2 +- data/human/free worlds 3 checkmate.txt | 4 ++-- data/human/free worlds 3 reconciliation.txt | 2 +- data/human/human missions.txt | 2 +- data/human/jobs.txt | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/data/bunrodea/bunrodea weapons.txt b/data/bunrodea/bunrodea weapons.txt index 5c82220e8cd4..ed060ae5319f 100644 --- a/data/bunrodea/bunrodea weapons.txt +++ b/data/bunrodea/bunrodea weapons.txt @@ -177,7 +177,7 @@ outfit "Swarm Clip" "outfit space" -10 "swarm capacity" 200 ammo "Swarm Missile" - description "Swarm Clips provide a sizeable increase in ammo capacity for Swarm Pods, holding a respectable 200 missiles able to be swiftly loaded into a launcher after its current clip is empty." + description "Swarm Clips provide a sizable increase in ammo capacity for Swarm Pods, holding a respectable 200 missiles able to be swiftly loaded into a launcher after its current clip is empty." outfit "Swarm Missile" category "Ammunition" diff --git a/data/human/free worlds 1 start.txt b/data/human/free worlds 1 start.txt index e8779c3f034a..135129af5b96 100644 --- a/data/human/free worlds 1 start.txt +++ b/data/human/free worlds 1 start.txt @@ -2264,7 +2264,7 @@ mission "FW Rogue Elements" event "navy occupying the south" 45 event "memorial on Deep" conversation - `After you land, you notice that a sizeable crowd has gathered around a video screen near the edge of the spaceport. You approach to find out what's going on. You're about to ask someone what's happening when you overhear Tomek's voice coming from screen. You look up to see a video of Tomek standing atop a Free Worlds warship and addressing an unseen crowd.` + `After you land, you notice that a sizable crowd has gathered around a video screen near the edge of the spaceport. You approach to find out what's going on. You're about to ask someone what's happening when you overhear Tomek's voice coming from screen. You look up to see a video of Tomek standing atop a Free Worlds warship and addressing an unseen crowd.` ` "For generations we have been neglected by the Republic!" he shouts. "Our grievances were ignored! Our pain and suffering was ignored! Our needs were ignored! We cried out for help, and yet the Republic did nothing, allowing our planets to be ravaged by piracy while continuing to tax our credits for services we were not provided. And as the years have gone on the pirates have only become more and more emboldened.` ` "We tried our best, even with the system stacked against us. Councilor Katya Reynolds and others like her spoke on the behalf of billions within the South, Rim, and Dirt Belt. On behalf of us. On behalf of our brothers and sisters, our fathers and mothers, our sons and daughters. And they spat in her face! And when they did that, they spat in our faces!` ` "And so we banded together. Friends and family locked in arms, doing that which the Republic would not. We looked out for each other, and raised our own defenses against the piracy that we so feared. And then we stood up together and told the Republic that we would take their neglect no longer. We shouted that the system does not work, and so we will create our own! We, with the sweat, blood, and tears of generations, birthed these Free Worlds!` diff --git a/data/human/free worlds 3 checkmate.txt b/data/human/free worlds 3 checkmate.txt index dbe4fe9adb52..c9bd9b847fb2 100644 --- a/data/human/free worlds 3 checkmate.txt +++ b/data/human/free worlds 3 checkmate.txt @@ -61,7 +61,7 @@ mission "FWC Attack Kaus Borealis" on offer event "fwc attack kaus borealis" conversation - `When you return to New Portland, a sizeable Free Worlds fleet is already parked on the landing pads surrounding the spaceport. JJ explains to you, "When the Navy struck and cut our territory in half, a lot of these ships were cut off from their chain of command and just went and sheltered wherever they could. So it took a bit of doing to gather them all together. And now what we need is another stirring victory, or they'll begin to scatter again."` + `When you return to New Portland, a sizable Free Worlds fleet is already parked on the landing pads surrounding the spaceport. JJ explains to you, "When the Navy struck and cut our territory in half, a lot of these ships were cut off from their chain of command and just went and sheltered wherever they could. So it took a bit of doing to gather them all together. And now what we need is another stirring victory, or they'll begin to scatter again."` choice ` "Are you saying we risk having members of our militia desert?"` goto desert @@ -2250,7 +2250,7 @@ mission "FWC Pug 6" ` "Sure, with the Navy at our backs I think we've got a good chance."` ` "As ready as I'll ever be, I guess."` ` She laughs. "I suppose that's the best we can hope for. Do you mind if I ride in your ship tomorrow, rather than one of the others? If this is the end, I'd rather face it with people I know." You agree. She turns back to look out the window.` - ` Since Freya seems to want to be alone, you return downstairs, and discover that a sizeable Oathkeeper fleet has arrived. In the main vestibule of the spaceport, an Oathkeeper officer is leading a group in qigong meditation. And surprisingly, you spot Admiral Danforth sitting with the Muslim prayer group and chanting in fluent Arabic. Meanwhile, the party outside the pub continues.` + ` Since Freya seems to want to be alone, you return downstairs, and discover that a sizable Oathkeeper fleet has arrived. In the main vestibule of the spaceport, an Oathkeeper officer is leading a group in qigong meditation. And surprisingly, you spot Admiral Danforth sitting with the Muslim prayer group and chanting in fluent Arabic. Meanwhile, the party outside the pub continues.` ` Eventually morning comes, and you prepare your ship for the assault on the Pug homeworld...` accept diff --git a/data/human/free worlds 3 reconciliation.txt b/data/human/free worlds 3 reconciliation.txt index 8bf95bdac3a0..577ebb147b52 100644 --- a/data/human/free worlds 3 reconciliation.txt +++ b/data/human/free worlds 3 reconciliation.txt @@ -2239,7 +2239,7 @@ mission "FW Pug 6" ` "Sure, with the Navy at our backs I think we've got a good chance."` ` "As ready as I'll ever be, I guess."` ` She laughs. "I suppose that's the best we can hope for. Do you mind if I ride in your ship tomorrow, rather than one of the others? If this is the end, I'd rather face it with people I know." You agree. She turns back to look out the window.` - ` Since Freya seems to want to be alone, you return downstairs, and discover that a sizeable Oathkeeper fleet has arrived. In the main vestibule of the spaceport, an Oathkeeper officer is leading a group in qigong meditation. And surprisingly, you spot Admiral Danforth sitting with the Muslim prayer group and chanting in fluent Arabic. Meanwhile, the party outside the pub continues.` + ` Since Freya seems to want to be alone, you return downstairs, and discover that a sizable Oathkeeper fleet has arrived. In the main vestibule of the spaceport, an Oathkeeper officer is leading a group in qigong meditation. And surprisingly, you spot Admiral Danforth sitting with the Muslim prayer group and chanting in fluent Arabic. Meanwhile, the party outside the pub continues.` ` Eventually morning comes, and you prepare your ship for the assault on the Pug homeworld...` accept diff --git a/data/human/human missions.txt b/data/human/human missions.txt index 7ff13684eb2d..867191e2efea 100644 --- a/data/human/human missions.txt +++ b/data/human/human missions.txt @@ -883,7 +883,7 @@ mission "Transport Workers C" payment payment 10000 dialog `You drop off the Pattersons, the farm family from , and help them to unload their wagon and cattle from your cargo hold. They thank you, and Jim pays you .` - log "Minor People" "Jim and Annette Patterson" `An unusual family who needed you to make a very unconventional trip with a sizeable number of farm animals that had to be kept happy in an enclosed area of your spaceship.` + log "Minor People" "Jim and Annette Patterson" `An unusual family who needed you to make a very unconventional trip with a sizable number of farm animals that had to be kept happy in an enclosed area of your spaceship.` diff --git a/data/human/jobs.txt b/data/human/jobs.txt index aa79aca96d95..44c2cf58f3ab 100644 --- a/data/human/jobs.txt +++ b/data/human/jobs.txt @@ -3313,7 +3313,7 @@ mission "Bounty Hunting (Small, Local)" mission "Bounty Hunting (Medium)" name "Wanted pirate near " - description "A sizeable pirate warship named the has been attacking merchants a jump away from the system. Destroy it by for payment ()." + description "A sizable pirate warship named the has been attacking merchants a jump away from the system. Destroy it by for payment ()." repeat job deadline 14 @@ -3351,7 +3351,7 @@ mission "Bounty Hunting (Medium)" mission "Bounty Hunting (Medium, Local)" name "Wanted pirate entering " - description "A sizeable pirate warship named the has been attacking merchants in nearby star systems, and is reportedly entering this system. Destroy it by for payment ()." + description "A sizable pirate warship named the has been attacking merchants in nearby star systems, and is reportedly entering this system. Destroy it by for payment ()." repeat job deadline 3 From 725f621130816dc2d7c496b231ff649d55824c99 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:43:13 +0300 Subject: [PATCH 05/75] fix(content): Fix typos in news lines (#10170) --- data/coalition/coalition news.txt | 2 +- data/human/news.txt | 3 +-- data/quarg/quarg news.txt | 2 +- data/wanderer/wanderer news.txt | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/data/coalition/coalition news.txt b/data/coalition/coalition news.txt index 103d50316a0f..08012f78f932 100644 --- a/data/coalition/coalition news.txt +++ b/data/coalition/coalition news.txt @@ -21,7 +21,7 @@ news "heliarch news" message word "A sizable crowd is watching one of the monitors in the spaceport where an interviewer is speaking to a Heliarch agent. You learn from someone in the crowd that the agent was a professional athlete until recently." - "A news broadcast shows footage of many Heliarch agents arresting a group of terrorists. The Heliarch who was supervising the operation is later interviewed, explaining that the now prisoners were found to have stolen government equipment." + "A news broadcast shows footage of many Heliarch agents arresting a group of terrorists. The Heliarch who was supervising the operation is later interviewed, explaining that the new prisoners were found to have stolen government equipment." "The news show a Heliarch consul giving a short speech in a ship launching ceremony. After they finish, the brand new Heliarch warships are shown heading to their first patrol." "All the spaceport screens momentarily swap to a broadcast from the Heliarch government, where they show a video of several dozens of arrested criminals. The Heliarch host describing the event mentions their sentences range from 40 years to life, prompting varying reactions from the civilian spectators." "During one of the more busy hours on the spaceport, the news details the crimes of a recently arrested terrorist group: arson, kidnapping, theft of government equipment, possession of weapons, conspiring against the Coalition, attacking both government and civilian properties, subversion... The list goes on for a few more minutes." diff --git a/data/human/news.txt b/data/human/news.txt index 364d9449a3f7..0801b05768a9 100644 --- a/data/human/news.txt +++ b/data/human/news.txt @@ -798,7 +798,7 @@ news "dentist" word `"` word - "Remember to pack a toothcleanser for your journey!" + "Remember to pack a toothcleaner for your journey!" "My new anti-cavity campaign is called 'Plaque Doesn't Deserve a Plaque.' Do you like it?" "If the galaxy has many alien species, I hope they all have teeth." "The advent of almost perfect artificial teeth may tempt you to stop caring if your natural teeth start to rot. Trust me that taking care of your teeth saves a lot of pain from both your mouth and your wallet." @@ -1006,7 +1006,6 @@ news "reporter" "Read this! 'The Real UGLIEST Starship that Lionheart Don't Want You to Know About!'" "Look at my new article! 'The Protector - Actually a SCAM?'" "Look at this! 'Five Reasons Why Captains Should Check Spaceports More Often - Number Two Will Make You Cry!'" - "Read my new article! 'Ten Things the Quarg Are Hiding from Us - Number Seven Will Surprise You!'" "Check this out! 'Top 20 Things Starship Pilots Don't Want You to Know!' Wait, never mind." "'11 signs that you are dealing with a corporate executive.' Read my blog to protect yourself from those psychopaths!" word diff --git a/data/quarg/quarg news.txt b/data/quarg/quarg news.txt index 1b9d22c65857..5cd5814940a2 100644 --- a/data/quarg/quarg news.txt +++ b/data/quarg/quarg news.txt @@ -31,7 +31,7 @@ news "anonymous quarg" word "a merchant" "a merchant captain" - "a travelling merchant" + "a traveling merchant" "a captain" word "?" diff --git a/data/wanderer/wanderer news.txt b/data/wanderer/wanderer news.txt index 1ca058861c7d..824c1c01d847 100644 --- a/data/wanderer/wanderer news.txt +++ b/data/wanderer/wanderer news.txt @@ -90,7 +90,7 @@ news "Wanderers and Aliens" "I have heard that Hai and human space are [linked, joined] with a wormhole. Could it have been made by [our caretakers, those that open the Eye]?" "The Unfettered Hai enter Wanderer [space, territory] using jump drives. Where did they get such [devices, technology]?" "Our species has [met, made contact] with many aliens before, but never so many at once." - "Exploration is important for a society. If we did not move and discover new planets, we would have [stagnated, shrivelled] long ago." + "Exploration is important for a society. If we did not move and discover new planets, we would have [stagnated, shriveled] long ago." "I can [sympathize with, understand] the Hai. They have been [stuck, stagnant] in their own space for too long. It is only [right, natural] that they should want to [migrate to, leave for] new lands." word `"` From 9255e27a71866cb032580bdd55fd8494aa7ee431 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Sat, 8 Jun 2024 07:08:29 +0300 Subject: [PATCH 06/75] content(fix): typo fixes in human hails (#10175) --- data/human/hails.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/human/hails.txt b/data/human/hails.txt index eefba70232d2..8d9ec3395a93 100644 --- a/data/human/hails.txt +++ b/data/human/hails.txt @@ -6321,7 +6321,7 @@ phrase "hostile pirate" "walk you out the airlock" "tear you in half" "break your bones" - "paralyse you" + "paralyze you" "take your credits" "sell you to the slave markets" "enslave you" @@ -6346,7 +6346,7 @@ phrase "hostile pirate" "sell you to Greenrock's fighting pits" "make you fight in the pits" "cover you in fire ants" - "operate on you without anaesthetic" + "operate on you without anesthetic" "cut your heart out" "use you as a test subject" "sacrifice you" From 5e4f2bf4676cef9a01c12f129d5dfed77b539ebb Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:12:10 +0200 Subject: [PATCH 07/75] fix(mechanics): Don't reset the player's flagship after death (#10162) --- source/PlayerInfo.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/PlayerInfo.cpp b/source/PlayerInfo.cpp index 90d7e6482831..7e3bff4a3db8 100644 --- a/source/PlayerInfo.cpp +++ b/source/PlayerInfo.cpp @@ -646,8 +646,6 @@ void PlayerInfo::Die(int response, const shared_ptr &capturer) if(it != ships.end()) ships.erase(it); } - - flagship.reset(); } From c88386d6d49244c4290d9348dc04f6c023def472 Mon Sep 17 00:00:00 2001 From: ziproot <109186806+ziproot@users.noreply.github.com> Date: Sat, 8 Jun 2024 13:44:03 -0400 Subject: [PATCH 08/75] fix(content): Give Hai festival jobs a deadline (#10148) --- data/hai/hai jobs.txt | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/data/hai/hai jobs.txt b/data/hai/hai jobs.txt index 40abcc7e37d8..455b0f801102 100644 --- a/data/hai/hai jobs.txt +++ b/data/hai/hai jobs.txt @@ -222,12 +222,21 @@ mission "Hai Festival [1]" name "Festival at " job repeat - description "For a month each year, the Hai have a huge holiday festival. This group of Hai will pay you for you to transport them and their of supplies to ." + description "For a month each year, the Hai have a huge holiday festival. This group of Hai will pay you for you to transport them and their of supplies to by ." + deadline 0 2 passengers 10 20 cargo "party supplies" 1 2 to offer - month == 3 + or + and + month == 3 + day <= 17 + and + month == 2 + day >= 27 random < 60 + to fail + month >= 4 source government "Hai" destination @@ -248,12 +257,21 @@ mission "Hai Festival [2]" name "Festival at " job repeat - description "For a month each year, the Hai have a huge holiday festival. This group of humans will pay you for you to transport them and their of supplies to ." + description "For a month each year, the Hai have a huge holiday festival. This group of humans will pay you for you to transport them and their of supplies to by ." + deadline 0 2 passengers 10 20 cargo "party supplies" 1 2 to offer - month == 3 + or + and + month == 3 + day <= 17 + and + month == 2 + day >= 27 random < 60 + to fail + month >= 4 source government "Hai" destination @@ -274,12 +292,21 @@ mission "Hai Festival [3]" name "Festival at " job repeat - description "For a month each year, the Hai have a huge holiday festival. This group of Hai and humans will pay you for you to transport them and their of supplies to ." + description "For a month each year, the Hai have a huge holiday festival. This group of Hai and humans will pay you for you to transport them and their of supplies to by ." + deadline 0 2 passengers 10 20 cargo "party supplies" 1 2 to offer - month == 3 + or + and + month == 3 + day <= 17 + and + month == 2 + day >= 27 random < 60 + to fail + month >= 4 source government "Hai" destination From 300f04bac42d556ccd2da2bda3cdc9c1540051e3 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Sat, 8 Jun 2024 21:17:04 +0200 Subject: [PATCH 09/75] feat(mechanics): Added a gamerule for controlling disabled fighter projectile collision (#10177) --- data/gamerules.txt | 5 +++++ source/AI.cpp | 6 +++++- source/CMakeLists.txt | 1 + source/Engine.cpp | 3 ++- source/FighterHitHelper.h | 43 +++++++++++++++++++++++++++++++++++++++ source/Gamerules.cpp | 19 +++++++++++++++++ source/Gamerules.h | 10 +++++++++ source/Projectile.cpp | 3 ++- 8 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 source/FighterHitHelper.h diff --git a/data/gamerules.txt b/data/gamerules.txt index 48c74393babe..d7a09439d14d 100644 --- a/data/gamerules.txt +++ b/data/gamerules.txt @@ -51,6 +51,11 @@ gamerules # ALLOWABLE VALUES: any value between 0. and 1. "universal frugal threshold" 0.75 + # Controls which fighters, when disabled, will not be hit by projectiles unless they are directly targeting. + # DEFAULT: all + # ALLOWABLE VALUES: all, none, only player + "disabled fighters avoid projectiles" "all" + # Depreciation related game rules: # The depreciated value of an item that is "age" days old is calculated with the following equation: diff --git a/source/AI.cpp b/source/AI.cpp index 8850f9c74817..29cc36afcb4f 100644 --- a/source/AI.cpp +++ b/source/AI.cpp @@ -18,6 +18,7 @@ this program. If not, see . #include "Audio.h" #include "Command.h" #include "DistanceMap.h" +#include "FighterHitHelper.h" #include "Flotsam.h" #include "GameData.h" #include "Gamerules.h" @@ -696,7 +697,7 @@ void AI::Step(Command &activeCommands) // focus on damaging one particular ship. targetTurn = (targetTurn + 1) & 31; if(targetTurn == step || !target || target->IsDestroyed() || (target->IsDisabled() && - (personality.Disables() || (target->CanBeCarried() && !personality.IsVindictive()))) + (personality.Disables() || (!FighterHitHelper::IsValidTarget(target.get()) && !personality.IsVindictive()))) || (target->IsFleeing() && personality.IsMerciful()) || !target->IsTargetable()) { target = FindTarget(*it); @@ -3685,6 +3686,9 @@ void AI::AutoFire(const Ship &ship, FireCommand &command, bool secondary, bool i // Merciful ships let fleeing ships go. if(target->IsFleeing() && person.IsMerciful()) continue; + // Don't hit ships that cannot be hit without targeting + if(target != currentTarget.get() && !FighterHitHelper::IsValidTarget(target)) + continue; Point p = target->Position() - start; Point v = target->Velocity(); diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 46c64ee579fe..93c8f9db2575 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -104,6 +104,7 @@ target_sources(EndlessSkyLib PRIVATE EscortDisplay.cpp EscortDisplay.h ExclusiveItem.h + FighterHitHelper.h File.cpp File.h Files.cpp diff --git a/source/Engine.cpp b/source/Engine.cpp index e6c0c66a59c4..8f4c7158cd29 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -25,6 +25,7 @@ this program. If not, see . #include "DamageDealt.h" #include "DamageProfile.h" #include "Effect.h" +#include "FighterHitHelper.h" #include "FillShader.h" #include "Fleet.h" #include "Flotsam.h" @@ -2239,7 +2240,7 @@ void Engine::DoCollisions(Projectile &projectile) // Don't collide with carried ships that are disabled and not directly targeted. if(shipHit && hit != projectile.Target() - && shipHit->CanBeCarried() && shipHit->IsDisabled()) + && !FighterHitHelper::IsValidTarget(shipHit.get())) continue; // If the ship is cloaked, and phasing, then skip this ship (during this step). diff --git a/source/FighterHitHelper.h b/source/FighterHitHelper.h new file mode 100644 index 000000000000..474861eb8dd1 --- /dev/null +++ b/source/FighterHitHelper.h @@ -0,0 +1,43 @@ +/* FighterHitHelper.h +Copyright (c) 2024 by tibetiroka + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#ifndef FIGHTER_HIT_POLICY_H_ +#define FIGHTER_HIT_POLICY_H_ + +#include "GameData.h" +#include "Gamerules.h" +#include "Ship.h" + + + +class FighterHitHelper +{ +public: + // Checks whether the given ship is a valid target for non-targeted projectiles. + static inline bool IsValidTarget(const Ship *ship) + { + if(!ship->CanBeCarried() || !ship->IsDisabled()) + return true; + switch(GameData::GetGamerules().FightersHitWhenDisabled()) + { + case Gamerules::FighterDodgePolicy::ALL: return false; + case Gamerules::FighterDodgePolicy::NONE: return true; + case Gamerules::FighterDodgePolicy::ONLY_PLAYER: return !ship->IsYours(); + } + return false; + } +}; + +#endif diff --git a/source/Gamerules.cpp b/source/Gamerules.cpp index 5573b603d618..376d08cd8862 100644 --- a/source/Gamerules.cpp +++ b/source/Gamerules.cpp @@ -54,6 +54,18 @@ void Gamerules::Load(const DataNode &node) depreciationGracePeriod = max(0, child.Value(1)); else if(key == "depreciation max age") depreciationMaxAge = max(0, child.Value(1)); + else if(key == "disabled fighters avoid projectiles") + { + const string &value = child.Token(1); + if(value == "all") + fighterHitPolicy = FighterDodgePolicy::ALL; + else if(value == "none") + fighterHitPolicy = FighterDodgePolicy::NONE; + else if(value == "only player") + fighterHitPolicy = FighterDodgePolicy::ONLY_PLAYER; + else + child.PrintTrace("Skipping unrecognized value for gamerule:"); + } else child.PrintTrace("Skipping unrecognized gamerule:"); } @@ -121,3 +133,10 @@ int Gamerules::DepreciationMaxAge() const { return depreciationMaxAge; } + + + +Gamerules::FighterDodgePolicy Gamerules::FightersHitWhenDisabled() const +{ + return fighterHitPolicy; +} diff --git a/source/Gamerules.h b/source/Gamerules.h index 5e731ad929d0..00ce92dde86f 100644 --- a/source/Gamerules.h +++ b/source/Gamerules.h @@ -23,6 +23,14 @@ class DataNode; // Gamerules contains a list of constants and booleans that define game behavior, // for example, the spawnrate of person ships or whether universal ramscoops are active. class Gamerules { +public: + // Defines which disabled fighters can dodge stray projectiles. + enum class FighterDodgePolicy + { + ALL, NONE, ONLY_PLAYER + }; + + public: Gamerules() = default; @@ -38,6 +46,7 @@ class Gamerules { double DepreciationDaily() const; int DepreciationGracePeriod() const; int DepreciationMaxAge() const; + FighterDodgePolicy FightersHitWhenDisabled() const; private: @@ -50,6 +59,7 @@ class Gamerules { double depreciationDaily = 0.997; int depreciationGracePeriod = 7; int depreciationMaxAge = 1000; + FighterDodgePolicy fighterHitPolicy = FighterDodgePolicy::ALL; }; diff --git a/source/Projectile.cpp b/source/Projectile.cpp index 83490c7d3161..130299cdcff3 100644 --- a/source/Projectile.cpp +++ b/source/Projectile.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "Projectile.h" #include "Effect.h" +#include "FighterHitHelper.h" #include "pi.h" #include "Random.h" #include "Ship.h" @@ -147,7 +148,7 @@ void Projectile::Move(vector &visuals, vector &projectiles) { target = TargetPtr().get(); if(!target || !target->IsTargetable() || target->GetGovernment() != targetGovernment || - (!targetDisabled && target->IsDisabled() && target->CanBeCarried())) + (!targetDisabled && !FighterHitHelper::IsValidTarget(target))) { BreakTarget(); target = nullptr; From 0691fd409fcfb2206219078ad964624b9781ec5f Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:11:28 +0200 Subject: [PATCH 10/75] fix(content): fix typo in `Garden Empyreal` description (#10184) --- data/map planets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/map planets.txt b/data/map planets.txt index 716b4d1e7aa9..009b7832bea8 100644 --- a/data/map planets.txt +++ b/data/map planets.txt @@ -1688,7 +1688,7 @@ planet "Garden Empyreal" attributes "coalition station" kimek shipping tourism wealthy landscape land/clouds_03 description `Not including the trio of Quarg ringworlds, this is the largest megastructure in Coalition space. Arrays of gigantic metal pyramids lay at every angle, each nearly a dozen times larger than the average space station. Spread out over the planet's upper atmosphere, these so-called "hubs" are connected by long, inextensible, and extremely sturdy wires, ensuring no part of the structure is destabilized.` - description ` First devised by the Kimek before the Coalition's war against the Quarg, the structure was completed relatively recently two thousand years ago. Populating the gas giant's uppermost atmosphere lay several hardy species of artificially engineered bacteria, periodically being "scooped" by the lower body of each hub and then later mass-produced into a nutritious soup.` + description ` First devised by the Kimek before the Coalition's war against the Quarg, the structure was completed relatively recently, about two thousand years ago. Populating the gas giant's uppermost atmosphere lay several hardy species of artificially engineered bacteria, periodically being "scooped" by the lower body of each hub and then later mass-produced into a nutritious soup.` spaceport `When not in "harvest season," the hubs are relatively quiet, and serve mostly as a tourist destination. Many come for the unique, close-up view of a sea of clouds, swarming with furious storms and dotted with more of the silver pyramids in the distance.` spaceport ` Each hub shares the same overall design in general, but the decoration pieces, services, and local amenities for each one have grown in their own way over the centuries. The only common thing they have between all of them is the signature restaurant, which serves a variety of dishes, drinks, and even desserts all based on the processed, scooped up produce.` "required reputation" 20 From a9fda1d0d7d84acaecba57b0ce2aa8119a1791c0 Mon Sep 17 00:00:00 2001 From: ziproot <109186806+ziproot@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:39:09 -0400 Subject: [PATCH 11/75] fix(content): Give Umbral a mark instead of a waypoint in Pirate Duel mission (#10094) --- data/human/human missions.txt | 51 ++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/data/human/human missions.txt b/data/human/human missions.txt index 867191e2efea..6beffdd0d2c5 100644 --- a/data/human/human missions.txt +++ b/data/human/human missions.txt @@ -3572,9 +3572,8 @@ mission "Pirate Duel" source near "Tarazed" 1 5 government "Republic" "Free Worlds" - waypoint "Umbral" + mark "Umbral" destination "Wayfarer" - to offer "combat rating" > 20 "combat rating" < 400 @@ -3631,18 +3630,64 @@ mission "Pirate Duel" on enter dialog `When you take off, you run a quick scan on all other ships that are taking off. You see no sign of the Esca. Maybe it's already waiting for you at Umbral.` on enter "Umbral" + unmark "Umbral" "reputation: Independent (Killable)" = -1000 conversation + branch "killed anglerfish" + has "anglerfish dead" + branch "disabled anglerfish" + has "anglerfish disabled" + branch "caught anglerfish" + has "anglerfish caught" `When you enter Umbral, you continue to see no sign of the Esca. Suddenly, an alarm rings out as your sensors detect that a pirate Vanguard has entered the system alongside you. Before you can respond, your ship intercepts a hail, and it opens communication.` ` The voice of the alleged captain of the Esca shouts out towards you. "Ho ho ho! Did you really think that I was the leader of the militia? Well, considering that you're here, I suppose you did. And now you'll pay for your mistake! Don't bother trying to call for help either, the militia don't patrol this system!"` ` The Vanguard powers up its weapons.` + decline + label "killed anglerfish" + `You enter Umbral, but see no sign of the Esca, as the "captain of the Esca" was killed. You should head back to Wayfarer to see if you can claim some sort of reward.` + decline + label "disabled anglerfish" + `You enter Umbral, but see no sign of the Esca, as the "captain of the Esca" was on the ship you disabled. You should head back to make sure the Anglerfish has been fully destroyed.` + decline + label "caught anglerfish" + action + clear "anglerfish caught" + `As you enter Umbral, the Anglerfish follows behind you, and opens a hail.` + ` "Did you think that if you went to Umbral, I would switch to a smaller ship? Ha!"` + ` The Anglerfish once again powers up its weapons.` + decline npc kill government "Independent (Killable)" personality heroic vindictive unconstrained launching confusion 20 ship "Vanguard (Plasma Slow)" "Anglerfish" - dialog `The Anglerfish and the "captain of the Esca" have been destroyed. You consider going back to Wayfarer to see if you can grab some sort of reward for your effort.` + on provoke + unmark "Umbral" + "reputation: Independent (Killable)" = -1000 + set "anglerfish caught" + conversation + `As soon as you open fire, the Anglerfish hails you.` + ` A voice you recognize as the "captain of the Esca" says, "How did you know that was me? Well, it doesn't matter, because I've paid off the militia. Get ready to die, small-fry."` + ` The Anglerfish powers up its weapons.` + decline + on disable + set "anglerfish disabled" + on kill + clear "anglerfish disabled" + set "anglerfish dead" + conversation + branch "orbiting tarazed" + has "flagship system: Tarazed" + branch "passed tarazed" + not "anglerfish caught" + `The Anglerfish and the "captain of the Esca" have been destroyed. You should continue on to Wayfarer to see if you can claim some sort of reward for your effort.` + decline + label "passed tarazed" + `The Anglerfish and the "captain of the Esca" have been destroyed. You should head back to Wayfarer to see if you can claim some sort of reward for your effort.` + decline + label "orbiting tarazed" + `The Anglerfish and the "captain of the Esca" have been destroyed. You should land on Wayfarer to see if you can claim some sort of reward for your effort.` on complete "reputation: Independent (Killable)" = 10 From 329d38a178024fd9faa30215af8cc7f98e96ad9b Mon Sep 17 00:00:00 2001 From: Nicholas Shanks Date: Fri, 14 Jun 2024 15:30:09 +0100 Subject: [PATCH 12/75] fix(mechanics): Make the 'ship destroyed' log appear earlier, and only once (#10165) --- source/Mission.cpp | 8 +++----- source/NPC.cpp | 3 ++- source/Ship.cpp | 10 ++++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/source/Mission.cpp b/source/Mission.cpp index deba6e9b922d..9c85ae283464 100644 --- a/source/Mission.cpp +++ b/source/Mission.cpp @@ -1191,8 +1191,7 @@ void Mission::Do(const ShipEvent &event, PlayerInfo &player, UI *ui) if(event.TargetGovernment()->IsPlayer() && !hasFailed) { bool failed = false; - string message = "Your " + event.Target()->DisplayModelName() + - " \"" + event.Target()->Name() + "\" has been "; + string message; if(event.Type() & ShipEvent::DESTROY) { // Destroyed ships carrying mission cargo result in failed missions. @@ -1202,8 +1201,6 @@ void Mission::Do(const ShipEvent &event, PlayerInfo &player, UI *ui) // If any mission passengers were present, this mission is failed. for(const auto &it : event.Target()->Cargo().PassengerList()) failed |= (it.first == this && it.second); - if(failed) - message += "destroyed. "; } else if(event.Type() & ShipEvent::BOARD) { @@ -1211,7 +1208,8 @@ void Mission::Do(const ShipEvent &event, PlayerInfo &player, UI *ui) for(const auto &it : event.Actor()->Cargo().MissionCargo()) failed |= (it.first == this); if(failed) - message += "plundered. "; + message = "Your " + event.Target()->DisplayModelName() + + " \"" + event.Target()->Name() + "\" has been plundered. "; } if(failed) diff --git a/source/NPC.cpp b/source/NPC.cpp index 7d2b119386ca..1df57e75fd0f 100644 --- a/source/NPC.cpp +++ b/source/NPC.cpp @@ -515,7 +515,8 @@ void NPC::Do(const ShipEvent &event, PlayerInfo &player, UI *ui, const Mission * // Check if the success status has changed. If so, display a message. if(isVisible && !alreadyFailed && HasFailed()) - Messages::Add("Mission failed.", Messages::Importance::Highest); + Messages::Add("Mission failed" + (caller ? ": \"" + caller->Name() + "\"" : "") + ".", + Messages::Importance::Highest); else if(ui && !alreadySucceeded && HasSucceeded(player.GetSystem(), false)) { // If "completing" this NPC displays a conversation, reference diff --git a/source/Ship.cpp b/source/Ship.cpp index e70b538caa64..88c88a197e86 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -3063,8 +3063,14 @@ int Ship::TakeDamage(vector &visuals, const DamageDealt &damage, const G hullDelay = max(hullDelay, static_cast(attributes.Get("disabled repair delay"))); } if(!wasDestroyed && IsDestroyed()) + { type |= ShipEvent::DESTROY; + if(IsYours() && Preferences::Has("Extra fleet status messages")) + Messages::Add("Your " + DisplayModelName() + + " \"" + Name() + "\" has been destroyed.", Messages::Importance::Highest); + } + // Inflicted heat damage may also disable a ship, but does not trigger a "DISABLE" event. if(heat > MaximumHeat()) { @@ -3730,10 +3736,6 @@ int Ship::StepDestroyed(vector &visuals, list> &flot // Once we've created enough little explosions, die. if(explosionCount == explosionTotal || forget) { - if(IsYours() && Preferences::Has("Extra fleet status messages")) - Messages::Add("Your " + DisplayModelName() + - " \"" + Name() + "\" has been destroyed.", Messages::Importance::Highest); - if(!forget) { const Effect *effect = GameData::Effects().Get("smoke"); From 0c667c415055d1b2db47ba775f97e4c228a7577e Mon Sep 17 00:00:00 2001 From: Nicholas Shanks Date: Fri, 14 Jun 2024 23:05:24 +0100 Subject: [PATCH 13/75] fix(ui): Properly warn when a ship is only running on batteries (#10174) --- source/Ship.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Ship.cpp b/source/Ship.cpp index 88c88a197e86..02f1260f5aaf 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -1302,7 +1302,7 @@ vector Ship::FlightCheck() const checks.emplace_back("afterburner only?"); if(!thrust && !afterburner) checks.emplace_back("reverse only?"); - if(!generation && !solar && !consuming) + if(energy <= battery) checks.emplace_back("battery only?"); if(energy < thrustEnergy) checks.emplace_back("limited thrust?"); From 4cc2489c64234bc0653236b6e81c3ffa1e6a740d Mon Sep 17 00:00:00 2001 From: ziproot <109186806+ziproot@users.noreply.github.com> Date: Fri, 14 Jun 2024 22:59:04 -0400 Subject: [PATCH 14/75] fix(content): Update Korath Worldship names for Remnant jobs (#10205) --- data/remnant/remnant 2 side missions.txt | 6 +++--- data/remnant/remnant jobs.txt | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data/remnant/remnant 2 side missions.txt b/data/remnant/remnant 2 side missions.txt index dbb621543440..2dbe389942a1 100644 --- a/data/remnant/remnant 2 side missions.txt +++ b/data/remnant/remnant 2 side missions.txt @@ -715,7 +715,7 @@ mission "Remnant: Will Not Someone Please Think Of The Children" minor landing name "Sympathy For The Korath" - description "Travel to the system to disable a stranded Kas'lor Ik 582. Do not destroy or capture the ship, as it likely has a large civilian population." + description "Travel to the system to disable a stranded Dathnak A'awoj. Do not destroy or capture the ship, as it likely has a large civilian population." source government "Remnant" waypoint "Vaticanus" @@ -730,7 +730,7 @@ mission "Remnant: Will Not Someone Please Think Of The Children" on offer conversation `You step out of your airlock to find Prefect Chilia waiting for you with a worried look on his face. "Captain , I have a sensitive request for you.` - ` "Our scouts are reporting a stranded Korath ship in Vaticanus, a Kas'lor Ik 582 to be precise. If it were a typical warship obstructing our operations, we would request you track it down and eliminate it, but because ships of this type tend to be home to a large civilian population, destroying or capturing it outright may instigate a reprisal, especially in that sector.` + ` "Our scouts are reporting a stranded Korath ship in Vaticanus, a Dathnak A'awoj to be precise. If it were a typical warship obstructing our operations, we would request you track it down and eliminate it, but because ships of this type tend to be home to a large civilian population, destroying or capturing it outright may instigate a reprisal, especially in that sector.` ` "Would you be up for disabling it, so the threat is eliminated while preserving the lives of the innocents on board?" His signs shift from a questioning tone to a confident one. "After you are done we can disarm the ship and fix their jump drive. If they adhere to their typical doctrine they should decide to head back home."` choice ` "Happy to help those in need, even the Korath."` @@ -761,7 +761,7 @@ mission "Remnant: Will Not Someone Please Think Of The Children" dialog `The Remnant are puzzled at your aborting of this mission, but they tell you they will send someone else to do this work. They warn you they will not propose any more of the sort to you again.` on fail "reputation: Remnant" -= 250 - dialog `You were unable to disable the Kas'lor Ik 582 without destroying it... The Remnant will not be pleased...` + dialog `You were unable to disable the Dathnak A'awoj without destroying it... The Remnant will not be pleased...` on complete "remnant: disable and save count" ++ payment 2500000 diff --git a/data/remnant/remnant jobs.txt b/data/remnant/remnant jobs.txt index 213ed74b8b56..0b659ed7d3bb 100644 --- a/data/remnant/remnant jobs.txt +++ b/data/remnant/remnant jobs.txt @@ -667,8 +667,8 @@ mission "Remnant: Rescue 5" mission "Remnant: Disable 1" job repeat - name "Disable Damaged Kas'lor Ik 582" - description "A damaged Kas'lor Ik 582 is stuck in a newly accessible system, near Postverta. It has a large civilian population so we ask you to only disable it to avoid an atrocity. Once you return to you will receive a logistics adjustment of ." + name "Disable Damaged Dathnak A'awoj" + description "A damaged Dathnak A'awoj is stuck in a newly accessible system, near Postverta. It has a large civilian population so we ask you to only disable it to avoid an atrocity. Once you return to you will receive a logistics adjustment of ." source government "Remnant" to offer @@ -685,15 +685,15 @@ mission "Remnant: Disable 1" names "korath" variant "Kas'lor Ik 582 (Stranded)" - dialog `You have disabled the Kas'lor Ik 582. Time to report back to .` + dialog `You have disabled the Dathnak A'awoj. Time to report back to .` on visit - dialog `You've landed on , but the stuck Kas'lor Ik 582 has not been disabled yet. Hunt it down and disable it before returning.` + dialog `You've landed on , but the stuck Dathnak A'awoj has not been disabled yet. Hunt it down and disable it before returning.` on abort dialog `The Remnant are puzzled at your aborting of this mission, but they tell you they will send someone else to do this work. They advise you they will prioritize other teams for these missions in the future.` on fail "reputation: Remnant" -= 250 - dialog `You were unable to disable the Kas'lor Ik 582 without destroying it... The Remnant will not be pleased...` + dialog `You were unable to disable the Dathnak A'awoj without destroying it... The Remnant will not be pleased...` on complete "remnant: disable and save count" ++ payment 2000000 - dialog "A Remnant military prefect thanks you for disabling down the Kas'lor Ik 582 , and gives you the agreed-upon logistics adjustment of ." + dialog "A Remnant military prefect thanks you for disabling the Dathnak A'awoj , and gives you the agreed-upon logistics adjustment of ." From 869d3c8b228a0c0bfc02ec95bb1dda9096df1a18 Mon Sep 17 00:00:00 2001 From: Saugia <93169396+Saugia@users.noreply.github.com> Date: Sun, 16 Jun 2024 15:30:30 -0400 Subject: [PATCH 15/75] fix(content): Correct sound reference for Benga Reverse Thruster (#10219) --- data/hai/hai outfits.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hai/hai outfits.txt b/data/hai/hai outfits.txt index 9df2a35d914e..6b91b667455b 100644 --- a/data/hai/hai outfits.txt +++ b/data/hai/hai outfits.txt @@ -709,7 +709,7 @@ outfit `"Benga" Reverse Thruster` "reverse thrusting heat" 4.8 "reverse flare sprite" "effect/atomic flare/tiny" "frame rate" 14 - "flare sound" "atomic tiny" + "reverse flare sound" "atomic tiny" description "Considering them to be well worth the steep cost, many Hai freighter pilots install reverse thrusters such as these to supplement the often poor steering on larger Hai vessels." outfit `"Biroo" Reverse Thruster` From 543a295f3438bbf6c7d14902e4b4f52a754b8209 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Sun, 16 Jun 2024 22:19:51 +0200 Subject: [PATCH 16/75] fix(mechanics): Fix crash when drawing cloaked ship and flagship outlines (#10190) --- source/Engine.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/Engine.cpp b/source/Engine.cpp index 8f4c7158cd29..c0820a9cc680 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -1116,6 +1116,8 @@ void Engine::Draw() const for(const auto &outline : outlines) { + if(!outline.sprite) + continue; Point size(outline.sprite->Width(), outline.sprite->Height()); OutlineShader::Draw(outline.sprite, outline.position, size, outline.color, outline.unit, outline.frame); } From 78e677419758d826b3f6008fb3b9906ba4fce417 Mon Sep 17 00:00:00 2001 From: warp-core Date: Mon, 17 Jun 2024 10:41:37 +0100 Subject: [PATCH 17/75] Fix steam linux build (#10215) --- CMakeLists.txt | 4 ++-- steam/docker-compose.yml | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1323b5006d0..618d121a0030 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,14 @@ cmake_minimum_required(VERSION 3.16...3.29) include(CMakeDependentOption) -if(UNIX AND NOT APPLE) +if(UNIX AND NOT APPLE AND NOT ES_STEAM) option(ES_USE_VCPKG "Use vcpkg to get dependencies." OFF) else() option(ES_USE_VCPKG "Use vcpkg to get dependencies." ON) endif() cmake_dependent_option(ES_GLES "Build the game with OpenGL ES" OFF UNIX OFF) cmake_dependent_option(ES_STEAM "Build the game for the Steam Linux runtime" OFF UNIX OFF) -cmake_dependent_option(ES_USE_SYSTEM_LIBRARIES "Use system libraries instead of the vcpkg ones." ON APPLE OFF) +cmake_dependent_option(ES_USE_SYSTEM_LIBRARIES "Use system libraries instead of the vcpkg ones." ON "APPLE OR ES_STEAM" OFF) cmake_dependent_option(ES_CREATE_BUNDLE "Create a Bundle instead of an executable. Not suitable for development purposes." OFF APPLE OFF) # Support Debug and Release configurations. diff --git a/steam/docker-compose.yml b/steam/docker-compose.yml index 353ef9915758..f36253a493fc 100644 --- a/steam/docker-compose.yml +++ b/steam/docker-compose.yml @@ -9,7 +9,6 @@ services: - '/bin/bash' - '-c' - | - apt-get -y update && apt-get -y install libmad0-dev mkdir -p build/steam-x64 cd build/steam-x64 cmake ../../ -GNinja -DES_STEAM=ON -DCMAKE_BUILD_TYPE=Release -DVCPKG_TARGET_TRIPLET=linux-x64-release-static @@ -25,7 +24,6 @@ services: - '/bin/bash' - '-c' - | - apt-get -y update && apt-get -y install libmad0-dev mkdir -p build/steam-x86 cd build/steam-x86 cmake ../../ -GNinja -DES_STEAM=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS=-m32 -DVCPKG_TARGET_TRIPLET=linux-x86-release-static From 17850854d1dce3c6721a85bab598e9dee5b183fe Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:35:04 +0300 Subject: [PATCH 18/75] fix(typo, issuetracker): Remove typo in the New Issue template (#10225) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 87c2e77db1fd..f679343e3b02 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,4 +5,4 @@ contact_links: about: Please report issues with the GitHub wiki here. - name: Endless Sky Mobile url: https://github.com/thewierdnut/endless-mobile/issues/ - about: Please report bugs any issues related to the mobile port here. + about: Please report any issues related to the mobile port here. From 8777384845239bcc0ac2bb95534056ac2c18e723 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:37:07 +0200 Subject: [PATCH 19/75] fix(typo): Fix typos in code comments (#10223) --- source/ConditionsStore.h | 2 +- source/DistanceMap.cpp | 2 +- source/Engine.cpp | 2 +- source/Government.h | 4 ++-- source/Interface.cpp | 2 +- source/Plugins.h | 2 +- source/TaskQueue.cpp | 4 ++-- source/TaskQueue.h | 2 +- source/Test.h | 2 +- source/text/Format.cpp | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/source/ConditionsStore.h b/source/ConditionsStore.h index 216663fd69df..19b3dd5fdacd 100644 --- a/source/ConditionsStore.h +++ b/source/ConditionsStore.h @@ -76,7 +76,7 @@ class ConditionsStore { }; - // Storage entry for a condition. Can act as a int64_t proxy when operator[] is used for access + // Storage entry for a condition. Can act as an int64_t proxy when operator[] is used for access // to conditions in the ConditionsStore. class ConditionEntry { friend ConditionsStore; diff --git a/source/DistanceMap.cpp b/source/DistanceMap.cpp index 1fb467dfb925..156da9592a7d 100644 --- a/source/DistanceMap.cpp +++ b/source/DistanceMap.cpp @@ -212,7 +212,7 @@ void DistanceMap::Init(const Ship *ship) } } - // Find the route with lowest fuel use. If multiple routes use the same fuel, + // Find the route with the lowest fuel use. If multiple routes use the same fuel, // choose the one with the fewest jumps (i.e. using jump drive rather than // hyperdrive). If multiple routes have the same fuel and the same number of // jumps, break the tie by using how "dangerous" the route is. diff --git a/source/Engine.cpp b/source/Engine.cpp index c0820a9cc680..15a464092ecc 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -2386,7 +2386,7 @@ void Engine::DoCollection(Flotsam &flotsam) // you'll be pushing the flotsam away from your ship, but the pull of the tractor beam // will still slowly close the distance between the ship and the flotsam. // When dealing with multiple ships, this causes a better appearance of a struggle between - // the ships all trying to get ahold of the flotsam should the ships all have similar velocities. + // the ships all trying to get a hold of the flotsam should the ships all have similar velocities. // If the ships have differing velocities, then it can make it look like the quicker ship is // yanking the flotsam away from the slower ship. pullVector += avgShipVelocity / count; diff --git a/source/Government.h b/source/Government.h index a1923eb55ad7..359dcda63ed1 100644 --- a/source/Government.h +++ b/source/Government.h @@ -41,7 +41,7 @@ class System; // Class representing a government. Each ship belongs to some government, and -// attacking that ship will provoke its ally governments and reduce your +// attacking that ship will provoke its allied governments and reduce your // reputation with them, but increase your reputation with that ship's enemies. // The ships for each government are identified by drawing them with a different // color "swizzle." Some government's ships can also be easier or harder to @@ -183,7 +183,7 @@ class Government { bool provokedOnScan = false; // If a government appears in this set, and the reputation with this government is affected by actions, // and events performed against that government, use the penalties that government applies for the - // action instead of this governments own penalties. + // action instead of this government's own penalties. std::set useForeignPenaltiesFor; }; diff --git a/source/Interface.cpp b/source/Interface.cpp index e531bfdfe43d..e072313fe463 100644 --- a/source/Interface.cpp +++ b/source/Interface.cpp @@ -536,7 +536,7 @@ Interface::TextElement::TextElement(const DataNode &node, const Point &globalAnc // This function will call ParseLine() for any line it does not recognize. Load(node, globalAnchor); - // Fill in any undefined state colors. By default labels are "medium", strings + // Fill in any undefined state colors. By default, labels are "medium", strings // are "bright", and button brightness depends on its activation state. if(!color[Element::ACTIVE] && !buttonKey) color[Element::ACTIVE] = GameData::Colors().Get(isDynamic ? "bright" : "medium"); diff --git a/source/Plugins.h b/source/Plugins.h index 07db9cf8027f..6f69d0a25345 100644 --- a/source/Plugins.h +++ b/source/Plugins.h @@ -37,7 +37,7 @@ struct Plugin { std::set required; // The plugins, if any, which are designed to work with this plugin but aren't required. std::set optional; - // The plugins, if any, which can't be run along side this plugin. + // The plugins, if any, which can't be run alongside this plugin. std::set conflicted; }; diff --git a/source/TaskQueue.cpp b/source/TaskQueue.cpp index 6754a00cd7d4..d89c99fa56ba 100644 --- a/source/TaskQueue.cpp +++ b/source/TaskQueue.cpp @@ -63,7 +63,7 @@ TaskQueue::~TaskQueue() -// Queue a function to execute in parallel, with an another optional function that +// Queue a function to execute in parallel, with another optional function that // will get executed on the main thread after the first function finishes. // Returns a future representing the future result of the async call. Ignores // any main thread task that still need to be executed! @@ -134,7 +134,7 @@ void TaskQueue::ThreadLoop() noexcept // Check whether it is time for this thread to quit. if(shouldQuit) return; - // No more tasks to execute, just to to sleep. + // No more tasks to execute, just go to sleep. if(tasks.empty()) break; diff --git a/source/TaskQueue.h b/source/TaskQueue.h index 1f60b974e820..17362eb5c5c8 100644 --- a/source/TaskQueue.h +++ b/source/TaskQueue.h @@ -57,7 +57,7 @@ class TaskQueue { TaskQueue &operator=(const TaskQueue &) = delete; ~TaskQueue(); - // Queue a function to execute in parallel, with an another optional function that + // Queue a function to execute in parallel, with another optional function that // will get executed on the main thread after the first function finishes. // Returns a future representing the future result of the async call. Ignores // any main thread task that still need to be executed! diff --git a/source/Test.h b/source/Test.h index 9dc04799300f..c766c068ec00 100644 --- a/source/Test.h +++ b/source/Test.h @@ -89,7 +89,7 @@ class Test { // checking asserts (similar to Conversations). ConditionSet conditions; // Labels to jump to in case of branches. We could optimize during - // load to lookup the step numbers (and provide integer stepnumbers + // load to look up the step numbers (and provide integer step numbers // here), but we can also use the textual information during error/ // debug printing, so keeping the strings for now. std::string jumpOnTrueTarget; diff --git a/source/text/Format.cpp b/source/text/Format.cpp index 4e3153952998..e3c60730ccd9 100644 --- a/source/text/Format.cpp +++ b/source/text/Format.cpp @@ -136,7 +136,7 @@ namespace { // Helper function for ExpandConditions. // - // source.substr(formatStart, formatSize) contains the format (credits, mass, etc) + // source.substr(formatStart, formatSize) contains the format (credits, mass, etc.) // source.substr(conditionStart, conditionSize) contains the condition name // // If formatStart or formatSize are string::npos, then there is no formatting. From 51de725e6eac78abf413e8025cc7e8efc3c91c7b Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:44:56 +0300 Subject: [PATCH 20/75] feat(balance): Make Wanderers: Mind 6 less difficult (#10189) --- data/wanderer/wanderers middle.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/wanderer/wanderers middle.txt b/data/wanderer/wanderers middle.txt index 50134ea6bf06..225400684e7c 100644 --- a/data/wanderer/wanderers middle.txt +++ b/data/wanderer/wanderers middle.txt @@ -1004,7 +1004,7 @@ mission "Wanderers: Mind Escorts" variant "Hurricane" 2 "Derecho" 3 - "Tempest" 4 + "Tempest" 5 mission "Wanderers: Mind 4" landing @@ -1127,9 +1127,9 @@ mission "Wanderers: Mind 6" variant "Model 512" 3 "Model 256" 4 - "Model 128" 5 - "Model 64" 6 - "Model 32" 8 + "Model 128" 4 + "Model 64" 5 + "Model 32" 7 "Model 16" 12 dialog `The last of the hostile Kor Mereti drones are destroyed; the remaining drones are not attacking the Wanderers. It is not entirely clear what has happened, but hopefully the Wanderers back on will have gained some information through the backdoor connection to their AI.` npc @@ -1139,10 +1139,10 @@ mission "Wanderers: Mind 6" names "kor mereti" variant "Model 256" 1 - "Model 128" 2 + "Model 128" 1 "Model 64" 3 "Model 32" 5 - "Model 16" 8 + "Model 16" 10 on visit dialog `You've landed on , but there are still hostile Kor Mereti drones in the Chimitarp system. Better depart and make sure they've been taken care of.` From 278b5475b83bf8bbf73c1aa5804b791d3c45d85c Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:58:36 +0300 Subject: [PATCH 21/75] fix(typo): FW typo fixes (#10198) --- data/human/free worlds 3 checkmate.txt | 8 ++++---- data/human/free worlds 3 reconciliation.txt | 16 ++++++++-------- data/human/free worlds 4 epilogue.txt | 8 ++++---- data/human/free worlds side plots.txt | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/data/human/free worlds 3 checkmate.txt b/data/human/free worlds 3 checkmate.txt index c9bd9b847fb2..5b068e6eabbe 100644 --- a/data/human/free worlds 3 checkmate.txt +++ b/data/human/free worlds 3 checkmate.txt @@ -1992,7 +1992,7 @@ mission "FWC Pug 4C" dialog `You have failed an essential Free Worlds mission. If you want to complete the story line, revert to the autosave or another earlier snapshot of the game.` on offer - event "fwc reconnect ascella" + event "fwc reconnect ascella" 0 conversation `You help Freya to unload and set up the reflector equipment, then contact the Navy on Oblivion and instruct them to point the transmitter in your direction. A few minutes later Admiral Danforth contacts you. "This link appears to be returning too," he says. "So, let's move on. We're going to make an attack on the system, now, and we would welcome your assistance in this battle." You leave the equipment on and return to your ship with Freya.` accept @@ -2001,7 +2001,7 @@ mission "FWC Pug 4C" dialog `You've landed on , but there are still Pug ships circling overhead. You should take off and help finish them off.` on complete log "Succeeded in restoring a few more hyperspace connections, and in driving back the Pug (with the help of the Navy). But, Freya thinks it is way too coincidental that the aliens have left behind the exact technology needed to fix the hyperspace disruptions; it feels like the Pug are playing a game rather than fighting a serious war." - event "fwc reconnect zeta aquilae" + event "fwc reconnect zeta aquilae" 0 dialog `After a few minutes of scanning the surface of , Freya locates another transmitter station that matches the one you found on Oblivion. You contact Admiral Danforth to tell him its location, and he dispatches a crew to take control of it. He also reports to you that once again, apparently all the Pug left the planet during the course of the battle. "A pity," he says. "If we had taken prisoners, maybe we could have gotten some answers from them. Anyway, meet me in the spaceport whenever you're ready to help with our next mission."` npc evade @@ -2072,7 +2072,7 @@ mission "FWC Pug 5" on offer conversation - `You find Admiral Danforth supervising the repairs of the damaged Navy ships. "Glad you survived that battle!" he says. "These alien ships are much more advanced than ours, but we outnumber them, so now that we've established a strong beachhead I think we actually have a chance at defeating them. The only problem is, the majority of our fleet is still up north. With the Vega system cut off, our supply lines are having to go all the way around through the dirt belt. So I'd like to propose liberating Vega next."` + `You find Admiral Danforth supervising the repairs of the damaged Navy ships. "Glad you survived that battle!" he says. "These alien ships are much more advanced than ours, but we outnumber them, so now that we've established a strong beachhead I think we actually have a chance at defeating them. The only problem is, the majority of our fleet is still up north. With the Vega system cut off, our supply lines are having to go all the way around through the Dirt Belt. So I'd like to propose liberating Vega next."` choice ` "That does sound like a good strategic choice."` ` "How will your fleet get to Vega without traveling through the Pug home system? That system is swarming with their ships."` @@ -2094,7 +2094,7 @@ mission "FWC Pug 5" on visit dialog `You've landed on , but there are still Pug ships circling overhead. You should take off and help finish them off.` on complete - event "fwc liberation of vega" + event "fwc liberation of vega" 0 npc government "Navy (Oathkeeper)" diff --git a/data/human/free worlds 3 reconciliation.txt b/data/human/free worlds 3 reconciliation.txt index 577ebb147b52..c58dcf1e72ee 100644 --- a/data/human/free worlds 3 reconciliation.txt +++ b/data/human/free worlds 3 reconciliation.txt @@ -801,7 +801,7 @@ mission "FW Syndicate Capture 1B" npc system Ruchbah government Syndicate - personality heroic staying nemesis + personality heroic nemesis staying fleet names "syndicate capital" variant @@ -1038,7 +1038,7 @@ mission "FW Pug 2" to offer has "FW Pug 1: done" passengers 1 - blocked `As you are landing, Alondo contacts you and says, "Do you have a bunk free for me?" You do not, so you will need to free one up in order to continue this mission.` + blocked `As you are landing, Alondo contacts you and asks, "Do you have a bunk free for me?" You do not, so you will need to free one up in order to continue this mission.` on fail dialog `You have failed an essential Free Worlds mission. If you want to complete the story line, revert to the autosave or another earlier snapshot of the game.` @@ -1101,16 +1101,16 @@ mission "FW Pug 2A" log "Agreed to travel to Syndicate space through the one remaining hyperspace path that is still open, through the uninhabited star systems to the north." conversation `A palpable sense of fear and near-panic hangs over the spaceport on Earth. At any minute, this system that serves as the very hub of human government could disappear into the same unknown abyss that has swallowed up its neighbors.` - ` Parliament is eager now to meet with you and to receive whatever assistance you may be able to offer. As Alondo stands in front of the chamber asking the members of Parliament for more information, they answer him with respect and deference, far different from how you have been treated here in the past. You learn that although one link remains open to Syndicate space, no one has dared to travel through it. Given the pace at which the other links were cut, they fear that the one remaining link will disappear any day now.` + ` Parliament is eager now to meet with you, and to receive whatever assistance you may be able to offer. As Alondo stands in front of the chamber asking the members of Parliament for more information, they answer him with respect and deference, far different from how you have been treated here in the past. You learn that although one link remains open to Syndicate space, no one has dared to travel through it. Given the pace at which the other links were cut, they fear that the one remaining link will disappear any day now.` ` As the conversation continues, it eventually becomes clear why you have been called here: the Republic wants you to take on the risk none of them is willing to face, and travel through to Syndicate space to reason with their leaders.` choice ` "If this is what it takes to keep human space from being torn into fragments, I will risk my ship to go meet with the Syndicate."` goto yes ` "This sounds like a job for the Navy, not for me. I can't risk being trapped in hostile Syndicate space forever."` goto no - ` "Did the Syndicate communicate anything to you before the links were cut, that might help us guess at their intentions?"` + ` "Did the Syndicate communicate anything to you before the links were cut that might help us guess at their intentions?"` - ` One of the members of Parliament says, "It's all terribly confusing. Our sources in Syndicate space were reporting massive fleet movements just before it happened. We also heard rumors of Syndicate fleets fighting each other, some sort of internal power struggle."` + ` One of the members of Parliament says, "It's all terribly confusing. Our sources in Syndicate space were reporting massive fleet movements just before it happened. We also heard rumors of Syndicate fleets fighting each other. Some sort of internal power struggle."` ` Another man speaks up and adds, "We had also been aware for some time that the Syndicate had gained access to alien technology, through encounters with a nomadic alien species currently inhabiting the galactic core. They may have found a way to use that technology to alter the links." From the surprised expressions on the faces of many of the other members of Parliament, it's clear that this information is new to them.` choice ` "Very well, we will accept this mission."` @@ -2019,7 +2019,7 @@ mission "FW Pug 4C" dialog `You have failed an essential Free Worlds mission. If you want to complete the story line, revert to the autosave or another earlier snapshot of the game.` on offer - event "reconnected delta capricorni" + event "reconnected delta capricorni" 0 conversation `Freya sent detailed instructions with the reflector for where it should be set up and how it should be adjusted to interface with the transmitter back in Pug space. You hand it over to a team of Syndicate engineers, who seem quite capable and confident that they can make it work; you suspect that these are the same engineers who have been playing with equipment stolen from the Korath, so they are no strangers to alien technology.` ` You also meet with Marco Bugati, the Syndicate executive who oversaw the installation of your jump drives, and tell him that you may soon be able to reopen one of the connections to Republic space, as well. You ask if he can send a courier ship to Earth to inform them of the new situation, to have them begin massing a fleet there. He agrees. He also tells you that a few of the Syndicate ships that met you in orbit have been designated as additions to your attack fleet, so they will be traveling back to Maker with you if the reflector succeeds in reopening the link.` @@ -2169,7 +2169,7 @@ mission "FW Pug 5B" on offer log "Succeeded in rendezvousing with the Navy in the Sol system and reopening a link to the Pug territory. But now Shiver is under attack by a massive Pug fleet." - event "reconnected altair" + event "reconnected altair" 0 conversation `You help some Navy engineers on Earth to set up the graviton reflector according to Freya's instructions. As before, soon after the reflector is operational the ground seems to tilt and roll as Freya's beam homes in on the reflector and sets up a harmonic oscillation between the two. "The link is reopening!" says one of the engineers. "We're beginning to receive hyperspace communications from Altair again, and from the Syndicate too."` ` A second later your own communicator beeps. It's Freya. "Get back here quickly," she says. "Bring whatever reinforcements you can. A massive Pug fleet just appeared in orbit."` @@ -2447,7 +2447,7 @@ mission "FW Syndicate Extremists 1A" ` "I don't see why not," says Danforth, fixing Alastair with a piercing gaze. "It's certainly better than leaving it in Syndicate hands."` label end - ` Alastair tells you that the device is being kept on Hephaestus, and the engineers there can install it in your ship. Meanwhile, Danforth will call in the rest of his Oathkeeper fleet. "And when we're through with this, I'll be seeing to it that this Oracle is destroyed so that this can never happen again," says Danforth. Alastair gives a meek nod in agreement. "Now, where are the Alphas."` + ` Alastair tells you that the device is being kept on Hephaestus, and the engineers there can install it in your ship. Meanwhile, Danforth will call in the rest of his Oathkeeper fleet. "And when we're through with this, I'll be seeing to it that this Oracle is destroyed so that this can never happen again," says Danforth. Alastair gives a meek nod in agreement. "Now, where are the Alphas?"` ` "The Alphas are holed up on Buccaneer Bay," says Alastair, "an old pirate world. Good luck in defeating them."` accept diff --git a/data/human/free worlds 4 epilogue.txt b/data/human/free worlds 4 epilogue.txt index a2b3ae1c5776..6e8756e37e20 100644 --- a/data/human/free worlds 4 epilogue.txt +++ b/data/human/free worlds 4 epilogue.txt @@ -45,7 +45,7 @@ mission "FW Epilogue: Ijs and Katya" label game ` "Whoops," says Ijs, "now you've done it, there goes the fourth wall."` - ` Katya laughs. "Well, there's already lots beyond human space to see, and I'm sure you can find more things to do out there as well as closer to home. If you're so eager to explore more of the galaxy, , or to discover what new adventures it has to offer, maybe you should volunteer to help out with creating the stories to populate it. This is an open source game after all, and it relies on people like you to bring it life."` + ` Katya laughs. "Well, there's already lots beyond human space to see, and I'm sure you can find more things to do out there as well as closer to home. If you're so eager to explore more of the galaxy, , or to discover what new adventures it has to offer, maybe you should volunteer to help out with creating the stories to populate it. This is an open source game after all, and it relies on people like you to bring it to life."` goto end label end @@ -189,7 +189,7 @@ mission "FW Epilogue: Danforth" label next ` "Are the pirates giving you any trouble?" you ask.` - ` "A bit," he says. "We still have far too many pirates in this sector, in part due to the Free Worlds driving them out of the South. The pirates grew stronger and more bold as the war dragged on, when all our fleets were busy elsewhere, but now we're beginning to get them under control again."` + ` "A bit," he says. "We still have far too many pirates in this sector, in part due to the Free Worlds mostly driving them out of the South. The pirates grew stronger and more bold as the war dragged on, when all our fleets were busy elsewhere, but now we're beginning to get them under control again."` branch checkmate2 has "free worlds checkmate" @@ -214,7 +214,7 @@ mission "FW Epilogue: Edward" has "free worlds plot completed" on offer conversation "edward epilogue" - npc save disable + npc disable save government "Test Dummy" personality disables heroic staying ship "Leviathan (Plasma Repeater)" "Hunk" @@ -230,7 +230,7 @@ mission "FW Epilogue: Edward" conversation "edward epilogue" branch completed has "FW Epilogue: Edward: done" - `As you complete your landing procedure, you recall having come here to assist in some testing for new weapons, back in the thick of the war. Do you want to check in on Barmy Edwards, the lead engineer?` + `As you complete your landing procedure, you recall having come here to assist in some testing for new weapons, back in the thick of the war. Do you want to check in on Barmy Edward, the lead engineer?` choice ` (Yes.)` ` (No.)` diff --git a/data/human/free worlds side plots.txt b/data/human/free worlds side plots.txt index 16e0e6d4a18d..1d10d02fe14c 100644 --- a/data/human/free worlds side plots.txt +++ b/data/human/free worlds side plots.txt @@ -376,8 +376,8 @@ mission "FW Flamethrower 2" ` "Great," he says, rubbing his hands together with a gleeful expression on his face. He's clearly looking forward to hearing what you think of his latest invention.` accept - npc save disable - personality disables staying heroic + npc disable save + personality disables heroic staying government "Test Dummy" ship "Doombat" "Doombat" From aabba7a57a3525c883157c40ab1834c2957caf33 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Wed, 19 Jun 2024 22:04:23 +0300 Subject: [PATCH 22/75] fix(typo): Make Excavator description consistent with its name (#10236) --- data/gegno/gegno outfits.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/gegno/gegno outfits.txt b/data/gegno/gegno outfits.txt index dbfc06b615cf..274728bcd2c8 100644 --- a/data/gegno/gegno outfits.txt +++ b/data/gegno/gegno outfits.txt @@ -819,7 +819,7 @@ outfit "Excavator" "minable damage" 104 "prospecting" 145 "hit force" -6 - description `Excavator Drills are large drills designed by the Gegno that rip materials out of asteroids in an efficient manner. These types of drill systems are only found on ships specifically designed around them as they require substantial infrastructure to be functional.` + description `Excavators are large drills designed by the Gegno that rip materials out of asteroids in an efficient manner. These types of drill systems are only found on ships specifically designed around them as they require substantial infrastructure to be functional.` effect "excavate" sprite "effect/smoke" From 54d66088676c5be07d3aaa257d1beb6207fe6668 Mon Sep 17 00:00:00 2001 From: warp-core Date: Thu, 20 Jun 2024 08:59:59 +0100 Subject: [PATCH 23/75] feat(ci): Add action to deploy wiki updates (#10115) --- .github/workflows/cd_wiki.yaml | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/cd_wiki.yaml diff --git a/.github/workflows/cd_wiki.yaml b/.github/workflows/cd_wiki.yaml new file mode 100644 index 000000000000..67fb2b80f728 --- /dev/null +++ b/.github/workflows/cd_wiki.yaml @@ -0,0 +1,55 @@ +name: Wiki CD + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +concurrency: + group: 'wiki-cd' + +jobs: + + cd_wiki: + name: Wiki deployment + runs-on: ubuntu-latest + if: ${{ github.repository == 'endless-sky/endless-sky' && github.ref == 'refs/heads/master' }} + permissions: + contents: write + steps: + - name: Checkout endless-sky-wiki repository + run: | + git clone "https://github.com/endless-sky/endless-sky-wiki.git" + - name: Checkout wiki + run: git clone "https://github.com/${{ github.repository }}.wiki.git" + - name: Synchronize files + run: rsync --delete -rvh -e .git endless-sky-wiki/wiki/* endless-sky.wiki/ + - name: Check for changes + id: check-diff + run: | + cd endless-sky.wiki + git add . + if [[ $(git diff --cached --raw) ]]; then + echo "changes=yes" >> "$GITHUB_OUTPUT" + fi + - name: Get latest commit hash + if: ${{ steps.check-diff.outputs.changes == 'yes' }} + id: latest-commit + run: | + cd endless-sky-wiki + echo "hash=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Commit changes + if: ${{ steps.check-diff.outputs.changes == 'yes' }} + run: | + cd endless-sky.wiki + git config --local user.name "GitHub Actions" + git config --local user.email "actions@github.com" + git commit -m "Synchronize with endless-sky-wiki" -m "Latest commit: endless-sky/endless-sky-wiki@${{ steps.latest-commit.outputs.hash }}" -m "Triggered by ${{ github.actor }}" + - name: Push changes + if: ${{ steps.check-diff.outputs.changes == 'yes' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd endless-sky.wiki + git remote set-url origin "https://github-actions:${{ env.GH_TOKEN }}@github.com/${{ github.repository }}.wiki.git" + git push origin From 68da360e03b207b84dcf0a0b0f818ecdb82654d0 Mon Sep 17 00:00:00 2001 From: TomGoodIdea <108272452+TomGoodIdea@users.noreply.github.com> Date: Sat, 22 Jun 2024 18:24:05 +0200 Subject: [PATCH 24/75] feat(mechanics): Illegal mission passengers can now be detected by planetary security (#10092) --- source/CargoHold.cpp | 14 +++++++++++--- source/CargoHold.h | 5 +++-- source/Mission.cpp | 22 +++++++++++----------- source/Mission.h | 8 ++++---- source/Politics.cpp | 41 +++++++++++++++++++++++++++++++++++------ 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/source/CargoHold.cpp b/source/CargoHold.cpp index 97ce906bdcb2..30e3d5564a12 100644 --- a/source/CargoHold.cpp +++ b/source/CargoHold.cpp @@ -610,16 +610,24 @@ int CargoHold::IllegalCargoFine(const Government *government, const PlayerInfo & // and avoid the bulk of the penalties when fined. for(const auto &it : missionCargo) { - int fine = it.first->IllegalCargoFine(); + int fine = it.first->Fine(); if(fine < 0) return fine; if(!it.first->IsFailed(player)) totalFine += fine; } + return totalFine; +} + + + +int CargoHold::IllegalPassengersFine(const Government *government, const PlayerInfo &player) const +{ + int totalFine = 0; for(const auto &it : passengers) { - int fine = it.first->IllegalCargoFine(); + int fine = it.first->Fine(); if(fine < 0) return fine; if(!it.first->IsFailed(player)) @@ -643,7 +651,7 @@ int CargoHold::IllegalCargoAmount() const // Find any illegal mission cargo. for(const auto &it : missionCargo) - if(it.first->IllegalCargoFine()) + if(it.first->Fine()) count += it.second; return count; diff --git a/source/CargoHold.h b/source/CargoHold.h index 5c8d4d069d13..87b59f4174cb 100644 --- a/source/CargoHold.h +++ b/source/CargoHold.h @@ -108,9 +108,10 @@ class CargoHold { // If anything you are carrying is illegal, return the maximum fine you can // be charged for any illegal outfits plus the sum of the fines for all - // missions. If the returned value is negative, you are carrying something so - // bad that it warrants a death sentence. + // missions. If the returned value is negative, you are carrying something + // or someone that warrants a death sentence for you. int IllegalCargoFine(const Government *government, const PlayerInfo &player) const; + int IllegalPassengersFine(const Government *government, const PlayerInfo &player) const; // Returns the amount tons of illegal cargo. int IllegalCargoAmount() const; diff --git a/source/Mission.cpp b/source/Mission.cpp index 9c85ae283464..d8cfdb259c5b 100644 --- a/source/Mission.cpp +++ b/source/Mission.cpp @@ -361,8 +361,8 @@ void Mission::Save(DataWriter &out, const string &tag) const out.Write("passengers", passengers); if(paymentApparent) out.Write("apparent payment", paymentApparent); - if(illegalCargoFine) - out.Write("illegal", illegalCargoFine, illegalCargoMessage); + if(fine) + out.Write("illegal", fine, fineMessage); if(failIfDiscovered) out.Write("stealth"); if(!isVisible) @@ -690,16 +690,16 @@ int Mission::CargoSize() const -int Mission::IllegalCargoFine() const +int Mission::Fine() const { - return illegalCargoFine; + return fine; } -string Mission::IllegalCargoMessage() const +string Mission::FineMessage() const { - return illegalCargoMessage; + return fineMessage; } @@ -1388,8 +1388,8 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b result.passengers = passengers; } result.paymentApparent = paymentApparent; - result.illegalCargoFine = illegalCargoFine; - result.illegalCargoMessage = Phrase::ExpandPhrases(illegalCargoMessage); + result.fine = fine; + result.fineMessage = Phrase::ExpandPhrases(fineMessage); result.failIfDiscovered = failIfDiscovered; result.distanceCalcSettings = distanceCalcSettings; @@ -1629,11 +1629,11 @@ bool Mission::Enter(const System *system, PlayerInfo &player, UI *ui) bool Mission::ParseContraband(const DataNode &node) { if(node.Token(0) == "illegal" && node.Size() == 2) - illegalCargoFine = node.Value(1); + fine = node.Value(1); else if(node.Token(0) == "illegal" && node.Size() == 3) { - illegalCargoFine = node.Value(1); - illegalCargoMessage = node.Token(2); + fine = node.Value(1); + fineMessage = node.Token(2); } else if(node.Token(0) == "stealth") failIfDiscovered = true; diff --git a/source/Mission.h b/source/Mission.h index 1994d69b5857..e3474e991429 100644 --- a/source/Mission.h +++ b/source/Mission.h @@ -103,8 +103,8 @@ class Mission { void Unmark(const System *system) const; const std::string &Cargo() const; int CargoSize() const; - int IllegalCargoFine() const; - std::string IllegalCargoMessage() const; + int Fine() const; + std::string FineMessage() const; bool FailIfDiscovered() const; int Passengers() const; int64_t DisplayedPayment() const; @@ -224,8 +224,8 @@ class Mission { // Parameters for generating random cargo amounts: int cargoLimit = 0; double cargoProb = 0.; - int illegalCargoFine = 0; - std::string illegalCargoMessage; + int fine = 0; + std::string fineMessage; bool failIfDiscovered = false; int passengers = 0; // Parameters for generating random passenger amounts: diff --git a/source/Politics.cpp b/source/Politics.cpp index 921c437f6767..559356d572d5 100644 --- a/source/Politics.cpp +++ b/source/Politics.cpp @@ -258,6 +258,35 @@ string Politics::Fine(PlayerInfo &player, const Government *gov, int scan, const int failedMissions = 0; + // Illegal passengers can only be detected by planetary security. + if(!scan) + { + int64_t fine = ship->Cargo().IllegalPassengersFine(gov, player); + if((fine > maxFine && maxFine >= 0) || fine < 0) + { + maxFine = fine; + reason = " for carrying illegal passengers."; + + for(const Mission &mission : player.Missions()) + { + if(mission.IsFailed(player)) + continue; + + string fineMessage = mission.FineMessage(); + if(!fineMessage.empty()) + { + reason = ".\n\t"; + reason.append(fineMessage); + } + // Fail any missions with illegal passengers and "stealth" set. + if(mission.Fine() > 0 && mission.Passengers() && mission.FailIfDiscovered()) + { + player.FailMission(mission); + ++failedMissions; + } + } + } + } if((!scan || (scan & ShipEvent::SCAN_CARGO)) && !EvadesCargoScan(*ship)) { int64_t fine = ship->Cargo().IllegalCargoFine(gov, player); @@ -271,15 +300,15 @@ string Politics::Fine(PlayerInfo &player, const Government *gov, int scan, const if(mission.IsFailed(player)) continue; - // Append the illegalCargoMessage from each applicable mission, if available - string illegalCargoMessage = mission.IllegalCargoMessage(); - if(!illegalCargoMessage.empty()) + // Append the fineMessage from each applicable mission, if available. + string fineMessage = mission.FineMessage(); + if(!fineMessage.empty()) { reason = ".\n\t"; - reason.append(illegalCargoMessage); + reason.append(fineMessage); } - // Fail any missions with illegal cargo and "Stealth" set - if(mission.IllegalCargoFine() > 0 && mission.FailIfDiscovered()) + // Fail any missions with illegal cargo and "stealth" set. + if(mission.Fine() > 0 && mission.CargoSize() && mission.FailIfDiscovered()) { player.FailMission(mission); ++failedMissions; From 3483d00741d4bb159cb72261358364eac007ffce Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Sat, 22 Jun 2024 18:30:22 +0200 Subject: [PATCH 25/75] fix(mechanics): Fix the `flagship only` flotsam collection setting (#10244) --- source/Engine.cpp | 2 +- source/Ship.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Engine.cpp b/source/Engine.cpp index 15a464092ecc..b019f58de3bd 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -2403,7 +2403,7 @@ void Engine::DoCollection(Flotsam &flotsam) return; if(collector == player.Flagship() && flotsamSetting == Preferences::FlotsamCollection::ESCORT) return; - if(flotsamSetting == Preferences::FlotsamCollection::FLAGSHIP) + if(collector != player.Flagship() && flotsamSetting == Preferences::FlotsamCollection::FLAGSHIP) return; } diff --git a/source/Ship.cpp b/source/Ship.cpp index 02f1260f5aaf..78f4170d7fd8 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -2085,7 +2085,7 @@ Point Ship::FireTractorBeam(const Flotsam &flotsam, vector &visuals) return pullVector; if(!GetParent() && flotsamSetting == Preferences::FlotsamCollection::ESCORT) return pullVector; - if(flotsamSetting == Preferences::FlotsamCollection::FLAGSHIP) + if(GetParent() && flotsamSetting == Preferences::FlotsamCollection::FLAGSHIP) return pullVector; } From d5dcdd6269d02dd6d0ef7fae0f4fe994936cfa6e Mon Sep 17 00:00:00 2001 From: warp-core Date: Sat, 22 Jun 2024 18:14:55 +0100 Subject: [PATCH 26/75] docs: 0.10.8 changelog + credits update (#10246) --- changelog | 53 ++++++++++++++++++- credits.txt | 5 +- endless-sky.6 | 2 +- io.github.endless_sky.endless_sky.appdata.xml | 16 ++++++ source/main.cpp | 2 +- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/changelog b/changelog index 1e1502e949d7..a82993ada898 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,55 @@ -version 0.10.7 +Version 0.10.8 + * Bug fixes: + * Content bugs: + * Typo fixes. (@eebop, @Hannah-E-M, @roadrunner56, @TheGiraffe3, @tibetiroka) + * Shortened Darkrest's description to better fit the planet description box. (@roadrunner56) + * Replaced duplicated words with weights in phrases. (@tibetiroka) + * The Hai festival jobs now have deadlines, as their descriptions suggested they should have had. (@ziproot) + * Updated Korath World-ship names for Remnant jobs. (@ziproot) + * The '"Benga" Reverse Thruster' now properly makes noise when in use. (@Saugia) + * Engine bugs: + * Fixed a crash when sorting ships without a ship selected with clang. (@warp-core) + * Fixed a crash with non-ASCII characters passed to cctype methods with clang. (@warp-core) + * Fixed a crash when sorting the ship list with no ships. (@warp-core) + * Fixed issues with the player interacting with the game after death. (@tibetiroka) + * Unicode byte order marks at the beginning of data files will no longer be tokenized. (@warp-core) + * The flagship will no longer move through wormholes ahead of escorts when using fleet jumping. (@Koranir) + * Fixed edge cases for the battery only flight check warning. (@nickshanks) + * Fixed a crash when drawing cloaked ship and flagship outlines. (@tibetiroka) + * The "flagship only" flotsam collection setting now works. (@tibetiroka) + * Game content: + * Mission changes: + * The Pact Recon missions will now no longer offer immediately after each other. (@Anarchist2) + * Reduced the pay and availability of Swiftsong jobs. (@roadrunner56) + * In the "Pirate Duel" mission, Umbral is now a marked system instead of a waypoint, meaning it is no longer required to visit the system in order to complete the mission. (@ziproot) + * Balance: + * Reduced "relative heat damage" sources by a factor of 1.5, for consistency with the mass rebalance in v0.10.7. (@Quantumshark) + * Reduced the difficulty of "Wanderers: Mind 6". (@TheGiraffe3) + * Wanderer escorts: + * +1 Tempest + * Friendly Mereti: + * -1 Model 128 + * +2 Model 16 + * Hostile Mereti: + * -1 Model 128 + * -1 Model 64 + * -1 Model 32 + * Game mechanics: + * New mechanics: + * Added a gamerule for controlling disabled fighter projectile collision. (@tibetiroka) + * Illegal mission passengers can now be detected by planetary security. Previously, missions with only illegal passengers could never result in a fine. (@TomGoodIdea) + * User interface: + * The message for a player escort being destroyed will now appear the moment the ship is destroyed instead of after it explodes and will not be duplicated. (@nickshanks) + * The message for the destruction of an escort will now include its model name. (@nickshanks) + * Salaries and tribute payments in the player info panel are now sorted in descending order. (@nickshanks) + * The mission description pane will no longer be shown if there is no mission selected. (@nickshanks) + * The scroll speed setting can now be configured to move from to 10 to 60 pixels at a time in steps of 10, instead of 20 to 60 pixels at a time in steps of 20. (@nickshanks) + * CI/CD and development environment: + * Updated the issue and PR templates to account for the new wiki repository. (@warp-core) + * Fixed the Steam Linux build. (@warp-core) + * Added an action to update the GitHub wiki from the endless-sky/endless-sky-wiki repository. (@warp-core) + +Version 0.10.7 * Big changes: * Rebalanced ship mass and engine performance across the game. (@Quantumshark) * Increased thrust, turn, reverse thrust, and afterburner thrust by 50% for all engines. diff --git a/credits.txt b/credits.txt index bd1aa1ff7d16..dae31642dbbd 100644 --- a/credits.txt +++ b/credits.txt @@ -1,5 +1,5 @@ Welcome to Endless Sky! -version 0.10.8-alpha +version 0.10.8 The player's manual and other resources are available at: @@ -247,6 +247,7 @@ contributed to Endless Sky: gunqqer Hacklin Hadron1776 + Hannah-E-M har9862 Hatrask Hecter94 @@ -325,6 +326,7 @@ contributed to Endless Sky: nathan-b Nescio0 neurotrope + nickshanks nobodywasishere NomadicVolcano nothing-but-the-rain @@ -412,6 +414,7 @@ contributed to Endless Sky: Terin The-Legendary-M thebigh2014 + TheGiraffe3 TheMarksman-ES thenerdfreak TheUnfetteredOne diff --git a/endless-sky.6 b/endless-sky.6 index d2ae516fe1d3..0a7c912db1f7 100644 --- a/endless-sky.6 +++ b/endless-sky.6 @@ -1,4 +1,4 @@ -.TH endless\-sky 6 "25 May 2024" "ver. 0.10.8-alpha" "Endless Sky" +.TH endless\-sky 6 "22 Jun 2024" "ver. 0.10.8" "Endless Sky" .SH NAME endless\-sky \- a space exploration and combat game. diff --git a/io.github.endless_sky.endless_sky.appdata.xml b/io.github.endless_sky.endless_sky.appdata.xml index a6b2f95b2e71..ce4c7d8dadfe 100644 --- a/io.github.endless_sky.endless_sky.appdata.xml +++ b/io.github.endless_sky.endless_sky.appdata.xml @@ -60,6 +60,22 @@ + + +

This is a stable release, focused on fixing bugs and making some other small improvements.

+

These changes include:

+
    +
  • Fixed various crashes, including ones that could occur when sorting ships or when using the flagship outline or new cloaked settings.
  • +
  • The flagship will no longer move through wormholes ahead of escorts when using fleet jumping.
  • +
  • Reduced the difficulty of the battle in "Wanderers: Mind 6".
  • +
  • Added a gamerule for controlling the disabled fighter projectile collision behavior introduced in v0.10.7.
  • +
  • Illegal mission passengers can now be detected by planetary security. Previously, missions with only illegal passengers could never result in a fine.
  • +
+

You can find out more in the changelog.

+

Special thanks to the 13 people who contributed to this release!

+
+ https://github.com/endless-sky/endless-sky/blob/v0.10.8/changelog +

This is an unstable release, containing big changes that may introduce new bugs.

diff --git a/source/main.cpp b/source/main.cpp index 0ee1ce6203d2..040fdd356c60 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -504,7 +504,7 @@ void PrintHelp() void PrintVersion() { cerr << endl; - cerr << "Endless Sky ver. 0.10.8-alpha" << endl; + cerr << "Endless Sky ver. 0.10.8" << endl; cerr << "License GPLv3+: GNU GPL version 3 or later: " << endl; cerr << "This is free software: you are free to change and redistribute it." << endl; cerr << "There is NO WARRANTY, to the extent permitted by law." << endl; From b1fc51609a3748c87630a68e9a7d35770fddb0f9 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Sun, 23 Jun 2024 03:07:58 +0300 Subject: [PATCH 27/75] feat(content): Nerve Gas to Marauder & Pirate ships (#10222) --- data/human/marauders.txt | 17 +++++++++++++++++ data/human/ships.txt | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/data/human/marauders.txt b/data/human/marauders.txt index bb2077261204..396e1d1c57a2 100644 --- a/data/human/marauders.txt +++ b/data/human/marauders.txt @@ -73,6 +73,7 @@ ship "Marauder Arrow" "Marauder Arrow (Engines)" "Supercapacitor" 2 "D14-RN Shield Generator" "Laser Rifle" 5 + "Nerve Gas" "A370 Atomic Thruster" "A375 Atomic Steering" @@ -259,6 +260,7 @@ ship "Marauder Falcon" "Liquid Helium Cooler" "Outfits Expansion" 3 "Laser Rifle" 80 + "Nerve Gas" 15 "Security Station" "A520 Atomic Thruster" @@ -337,6 +339,7 @@ ship "Marauder Falcon" "Marauder Falcon (Weapons)" "Liquid Helium Cooler" "Outfits Expansion" 3 "Laser Rifle" 80 + "Nerve Gas" 20 "A370 Atomic Thruster" "A525 Atomic Steering" @@ -394,6 +397,7 @@ ship "Marauder Firebird" "Liquid Helium Cooler" "Outfits Expansion" "Laser Rifle" 25 + "Nerve Gas" 29 "A250 Atomic Thruster" "A525 Atomic Steering" @@ -433,6 +437,7 @@ ship "Marauder Firebird" "Marauder Firebird (Engines)" "Outfits Expansion" "Security Station" "Laser Rifle" 24 + "Nerve Gas" "A370 Atomic Thruster" "A525 Atomic Steering" @@ -460,6 +465,7 @@ ship "Marauder Firebird" "Marauder Firebird (Weapons)" "Outfits Expansion" "Security Station" "Laser Rifle" 24 + "Nerve Gas" 13 "A250 Atomic Thruster" "A525 Atomic Steering" @@ -512,6 +518,7 @@ ship "Marauder Leviathan" "Liquid Helium Cooler" "Outfits Expansion" 2 "Laser Rifle" 69 + "Nerve Gas" 15 "A370 Atomic Thruster" "A525 Atomic Steering" @@ -555,6 +562,7 @@ ship "Marauder Leviathan" "Marauder Leviathan (Engines)" "Liquid Nitrogen Cooler" "Outfits Expansion" 2 "Laser Rifle" 69 + "Nerve Gas" 8 "A520 Atomic Thruster" "A865 Atomic Steering" @@ -585,6 +593,7 @@ ship "Marauder Leviathan" "Marauder Leviathan (Weapons)" "Liquid Helium Cooler" "Outfits Expansion" 2 "Laser Rifle" 69 + "Nerve Gas" 48 "A370 Atomic Thruster" "A525 Atomic Steering" @@ -639,6 +648,7 @@ ship "Marauder Manta" "Liquid Nitrogen Cooler" "Security Station" 3 "Laser Rifle" 8 + "Nerve Gas" 4 "A250 Atomic Thruster" "A525 Atomic Steering" @@ -713,6 +723,7 @@ ship "Marauder Manta" "Marauder Manta (Weapons)" "D14-RN Shield Generator" "Liquid Nitrogen Cooler" "Laser Rifle" 11 + "Nerve Gas" 2 "A250 Atomic Thruster" "A375 Atomic Steering" @@ -801,6 +812,7 @@ ship "Marauder Quicksilver" "Marauder Quicksilver (Engines)" "Liquid Nitrogen Cooler" "Security Station" 2 "Laser Rifle" 5 + "Nerve Gas" "A370 Atomic Thruster" "A375 Atomic Steering" @@ -931,6 +943,7 @@ ship "Marauder Raven" "Volcano Afterburner" "Hyperdrive" "Laser Rifle" 14 + "Nerve Gas" 3 engine -9.5 63 engine 9.5 63 @@ -966,6 +979,7 @@ ship "Marauder Raven" "Marauder Raven (Engines)" "A375 Atomic Steering" "Hyperdrive" "Laser Rifle" 14 + "Nerve Gas" engine -10.5 63.5 engine 10.5 63.5 @@ -994,6 +1008,7 @@ ship "Marauder Raven" "Marauder Raven (Weapons)" "A375 Atomic Steering" "Hyperdrive" "Laser Rifle" 14 + "Nerve Gas" 5 gun -13.5 -35.5 gun 13.5 -35.5 @@ -1038,6 +1053,7 @@ ship "Marauder Splinter" "D67-TM Shield Generator" "Liquid Nitrogen Cooler" "Laser Rifle" 22 + "Nerve Gas" 2 "A250 Atomic Thruster" "A525 Atomic Steering" @@ -1105,6 +1121,7 @@ ship "Marauder Splinter" "Marauder Splinter (Weapons)" "Liquid Nitrogen Cooler" "Security Station" 2 "Laser Rifle" 20 + "Nerve Gas" 4 "A250 Atomic Thruster" "A525 Atomic Steering" diff --git a/data/human/ships.txt b/data/human/ships.txt index bbfb1f9cb6ef..893f852772e4 100644 --- a/data/human/ships.txt +++ b/data/human/ships.txt @@ -840,6 +840,7 @@ ship "Bulwark" "Water Coolant System" "Tactical Scanner" "Laser Rifle" 19 + "Nerve Gas" 3 "Impala Plasma Thruster" "Impala Plasma Steering" @@ -2752,7 +2753,7 @@ ship "Modified Argosy" "D23-QP Shield Generator" "Small Radar Jammer" "Brig" - "Laser Rifle" 2 + "Laser Rifle" 12 "Greyhound Plasma Thruster" "X3200 Ion Steering" @@ -3935,6 +3936,7 @@ ship "Valkyrie" "D67-TM Shield Generator" "Large Radar Jammer" "Laser Rifle" 18 + "Nerve Gas" 3 "Greyhound Plasma Thruster" "Greyhound Plasma Steering" From 9deef1b4b9bcdd1c8f38d9b7eb3acc41f276fc6f Mon Sep 17 00:00:00 2001 From: warp-core Date: Sun, 23 Jun 2024 07:45:48 +0100 Subject: [PATCH 28/75] docs: 0.10.9-alpha version numbers (#10256) --- CMakeLists.txt | 2 +- credits.txt | 2 +- endless-sky.6 | 2 +- resources/EndlessSky-Info.plist | 2 +- source/main.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 618d121a0030..29a3a4c779a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT EndlessSky) set(CMAKE_VS_JUST_MY_CODE_DEBUGGING ON) -project("Endless Sky" VERSION 0.10.8 +project("Endless Sky" VERSION 0.10.9 DESCRIPTION "Space exploration, trading, and combat game." HOMEPAGE_URL https://endless-sky.github.io/ LANGUAGES CXX) diff --git a/credits.txt b/credits.txt index dae31642dbbd..d65b0b296fae 100644 --- a/credits.txt +++ b/credits.txt @@ -1,5 +1,5 @@ Welcome to Endless Sky! -version 0.10.8 +version 0.10.9-alpha The player's manual and other resources are available at: diff --git a/endless-sky.6 b/endless-sky.6 index 0a7c912db1f7..ba66c2f051a7 100644 --- a/endless-sky.6 +++ b/endless-sky.6 @@ -1,4 +1,4 @@ -.TH endless\-sky 6 "22 Jun 2024" "ver. 0.10.8" "Endless Sky" +.TH endless\-sky 6 "22 Jun 2024" "ver. 0.10.9-alpha" "Endless Sky" .SH NAME endless\-sky \- a space exploration and combat game. diff --git a/resources/EndlessSky-Info.plist b/resources/EndlessSky-Info.plist index 3a4f6a51238c..025cc053d83a 100644 --- a/resources/EndlessSky-Info.plist +++ b/resources/EndlessSky-Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.10.8 + 0.10.9 CFBundleSignature ???? CFBundleVersion diff --git a/source/main.cpp b/source/main.cpp index 040fdd356c60..88e56b11cb9d 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -504,7 +504,7 @@ void PrintHelp() void PrintVersion() { cerr << endl; - cerr << "Endless Sky ver. 0.10.8" << endl; + cerr << "Endless Sky ver. 0.10.9-alpha" << endl; cerr << "License GPLv3+: GNU GPL version 3 or later: " << endl; cerr << "This is free software: you are free to change and redistribute it." << endl; cerr << "There is NO WARRANTY, to the extent permitted by law." << endl; From f7a265143718ab8f2347af8a5b8736adc30bdaa8 Mon Sep 17 00:00:00 2001 From: Quantumshark Date: Sun, 23 Jun 2024 07:47:09 +0100 Subject: [PATCH 29/75] fix(typo): sting -> stings in wanderer missions (#10260) --- data/wanderer/wanderer missions.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/wanderer/wanderer missions.txt b/data/wanderer/wanderer missions.txt index 469167b0dbed..25a884f5ba86 100644 --- a/data/wanderer/wanderer missions.txt +++ b/data/wanderer/wanderer missions.txt @@ -85,7 +85,7 @@ mission "Wanderers: Mereti: The Plant 3" has "Wanderers: Mereti: The Plant 2: done" on offer conversation - `Va'ika guides you to an area of high, cool steppes as you approach. You find a suitable place to land your ship, and she steps out. The air that blows in through the open door sting your eyes, but Va'ika seems too deep in thought to notice. "It would have [grown, originated] here, I am certain of it. But it will do so no longer, not for many [years, cycles]." Va'ika bows her head and holds one wing in front of herself silently for a moment before continuing. "We must find some world with similar [regions, environments] that we can introduce it to. Perhaps we will have better luck on Sopi Lefarkata."` + `Va'ika guides you to an area of high, cool steppes as you approach. You find a suitable place to land your ship, and she steps out. The air that blows in through the open door stings your eyes, but Va'ika seems too deep in thought to notice. "It would have [grown, originated] here, I am certain of it. But it will do so no longer, not for many [years, cycles]." Va'ika bows her head and holds one wing in front of herself silently for a moment before continuing. "We must find some world with similar [regions, environments] that we can introduce it to. Perhaps we will have better luck on Sopi Lefarkata."` accept mission "Wanderers: Mereti: The Plant 4" From b9927b07affee70b4a3739b4855d7d5d7fe2c19c Mon Sep 17 00:00:00 2001 From: Unordered Sigh <116329264+UnorderedSigh@users.noreply.github.com> Date: Sun, 23 Jun 2024 03:34:00 -0400 Subject: [PATCH 30/75] feat(enhancement): `to display` in dialog text (#8504) --- source/Mission.cpp | 10 +-- source/MissionAction.cpp | 151 ++++++++++++++++++++++++++++++++------- source/MissionAction.h | 31 +++++++- source/NPC.cpp | 6 +- source/NPC.h | 4 +- source/NPCAction.cpp | 4 +- source/NPCAction.h | 2 +- 7 files changed, 168 insertions(+), 40 deletions(-) diff --git a/source/Mission.cpp b/source/Mission.cpp index d8cfdb259c5b..795b0f08bc21 100644 --- a/source/Mission.cpp +++ b/source/Mission.cpp @@ -1481,7 +1481,8 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b return result; } for(const NPC &npc : npcs) - result.npcs.push_back(npc.Instantiate(subs, sourceSystem, result.destination->GetSystem(), jumps, payload)); + result.npcs.push_back(npc.Instantiate(player.Conditions(), subs, + sourceSystem, result.destination->GetSystem(), jumps, payload)); // Instantiate the actions. The "complete" action is always first so that // the "" substitution can be filled in. @@ -1499,7 +1500,7 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b return result; } for(const auto &it : actions) - result.actions[it.first] = it.second.Instantiate(subs, sourceSystem, jumps, payload); + result.actions[it.first] = it.second.Instantiate(player.Conditions(), subs, sourceSystem, jumps, payload); auto oit = onEnter.begin(); for( ; oit != onEnter.end(); ++oit) @@ -1515,7 +1516,7 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b return result; } for(const auto &it : onEnter) - result.onEnter[it.first] = it.second.Instantiate(subs, sourceSystem, jumps, payload); + result.onEnter[it.first] = it.second.Instantiate(player.Conditions(), subs, sourceSystem, jumps, payload); auto eit = genericOnEnter.begin(); for( ; eit != genericOnEnter.end(); ++eit) @@ -1531,7 +1532,8 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b return result; } for(const MissionAction &action : genericOnEnter) - result.genericOnEnter.emplace_back(action.Instantiate(subs, sourceSystem, jumps, payload)); + result.genericOnEnter.emplace_back(action.Instantiate( + player.Conditions(), subs, sourceSystem, jumps, payload)); // Perform substitution in the name and description. result.displayName = Format::Replace(Phrase::ExpandPhrases(displayName), subs); diff --git a/source/MissionAction.cpp b/source/MissionAction.cpp index 82b5dbe1e4cb..ef03b490b123 100644 --- a/source/MissionAction.cpp +++ b/source/MissionAction.cpp @@ -57,6 +57,62 @@ namespace { +MissionAction::MissionDialog::MissionDialog(const ExclusiveItem &phrase): + dialogPhrase(phrase) +{ +} + + + +MissionAction::MissionDialog::MissionDialog(const string &text): + dialogText(text) +{ +} + + + +MissionAction::MissionDialog::MissionDialog(const DataNode &node) +{ + // Handle anonymous phrases + // phrase + // ... + if(node.Size() == 1 && node.Token(0) == "phrase") + { + dialogPhrase = ExclusiveItem(Phrase(node)); + // Anonymous phrases do not support "to display" + return; + } + + // Handle named phrases + // phrase "A Phrase Name" + if(node.Size() == 2 && node.Token(0) == "phrase") + dialogPhrase = ExclusiveItem(GameData::Phrases().Get(node.Token(1))); + + // Handle regular dialog text + // "Some thrilling dialog that truly moves the player." + else + { + if(node.Size() > 1) + node.PrintTrace("Ignoring extra tokens."); + dialogText = node.Token(0); + + // Prevent a corner case that breaks assumptions. Dialog text cannot be empty (that indicates a phrase). + if(dialogText.empty()) + dialogText = '\t'; + } + + // Search for "to display" lines. + for(auto &child : node) + { + if(child.Size() != 2 || child.Token(0) != "to" || child.Token(1) != "display" || !child.HasChildren()) + node.PrintTrace("Ignoring unrecognized dialog token"); + else + condition.Load(child); + } +} + + + // Construct and Load() at the same time. MissionAction::MissionAction(const DataNode &node) { @@ -74,6 +130,12 @@ void MissionAction::Load(const DataNode &node) for(const DataNode &child : node) LoadSingle(child); + + // Collapse pure-text dialog (no conditions or phrases). This is necessary to handle saved missions. + // It is also an optimization for the most common case in game data files. + dialogText = CollapseDialog(nullptr, nullptr); + if(!dialogText.empty()) + dialog.clear(); } @@ -85,23 +147,14 @@ void MissionAction::LoadSingle(const DataNode &child) if(key == "dialog") { - if(hasValue && child.Token(1) == "phrase") - { - if(!child.HasChildren() && child.Size() == 3) - dialogPhrase = ExclusiveItem(GameData::Phrases().Get(child.Token(2))); - else - child.PrintTrace("Skipping unsupported dialog phrase syntax:"); - } - else if(!hasValue && child.HasChildren() && (*child.begin()).Token(0) == "phrase") - { - const DataNode &firstGrand = (*child.begin()); - if(firstGrand.Size() == 1 && firstGrand.HasChildren()) - dialogPhrase = ExclusiveItem(Phrase(firstGrand)); - else - firstGrand.PrintTrace("Skipping unsupported dialog phrase syntax:"); - } - else - Dialog::ParseTextNode(child, 1, dialogText); + // Parse the "dialog phrase whatever" and "dialog whatever" lines: + if(child.Size() == 3 && child.Token(1) == "phrase") + dialog.emplace_back(ExclusiveItem(GameData::Phrases().Get(child.Token(2)))); + else if(hasValue) + dialog.emplace_back(child.Token(1)); + // Parse embedded child dialog + for(const auto &grand : child) + dialog.emplace_back(grand); } else if(key == "conversation" && child.HasChildren()) conversation = ExclusiveItem(Conversation(child)); @@ -188,10 +241,6 @@ string MissionAction::Validate() const if(!systemFilter.IsValid()) return "system location filter"; - // Stock phrases that generate text must be defined. - if(dialogPhrase.IsStock() && dialogPhrase->IsEmpty()) - return "stock phrase"; - // Stock conversations must be defined. if(conversation.IsStock() && conversation->IsEmpty()) return "stock conversation"; @@ -353,7 +402,7 @@ void MissionAction::Do(PlayerInfo &player, UI *ui, const Mission *caller, const // Convert this validated template into a populated action. -MissionAction MissionAction::Instantiate(map &subs, const System *origin, +MissionAction MissionAction::Instantiate(const ConditionsStore &store, map &subs, const System *origin, int jumps, int64_t payload) const { MissionAction result; @@ -369,9 +418,7 @@ MissionAction MissionAction::Instantiate(map &subs, const System result.action = action.Instantiate(subs, jumps, payload); // Create any associated dialog text from phrases, or use the directly specified text. - string dialogText = !dialogPhrase->IsEmpty() ? dialogPhrase->Get() : this->dialogText; - if(!dialogText.empty()) - result.dialogText = Format::Replace(Phrase::ExpandPhrases(dialogText), subs); + result.dialogText = CollapseDialog(&store, &subs); if(!conversation->IsEmpty()) result.conversation = ExclusiveItem(conversation->Instantiate(subs, jumps, payload)); @@ -392,3 +439,57 @@ int64_t MissionAction::Payment() const noexcept { return action.Payment(); } + + + +string MissionAction::CollapseDialog(const ConditionsStore *store, const map *subs) const +{ + // No store or subs means we're determining whether the dialog is pure text. + // This is done at load time. + bool loadTimeScan = !store || !subs; + + // Result is already cached for dialogs that are pure text at Load() time. + if(!dialogText.empty()) + { + if(loadTimeScan) + return dialogText; + else + return Format::Replace(Phrase::ExpandPhrases(dialogText), *subs); + } + + string resultText; + for(auto &item : dialog) + { + // When checking for a pure-text dialog, reject a dialog with conditions or phrases, + // An empty string return value tells the caller that this dialog isn't pure text. + if(loadTimeScan && (!item.condition.IsEmpty() || item.dialogText.empty())) + return string(); + + // Skip text that is disabled. + if(!item.condition.IsEmpty() && !item.condition.Test(*store)) + continue; + + // Evaluate the phrase if we have one, otherwise copy the prepared text. + string content; + if(!item.dialogText.empty()) + content = item.dialogText; + else if(item.dialogPhrase.IsStock() && item.dialogPhrase->IsEmpty()) + content = "stock phrase"; + else + content = item.dialogPhrase->Get(); + + // Expand any ${phrases} and + if(!loadTimeScan) + content = Format::Replace(Phrase::ExpandPhrases(content), *subs); + + // Concatenated lines should start with a tab and be preceeded by end-of-line. + if(!resultText.empty()) + { + resultText += '\n'; + if(!content.empty() && content[0] != '\t') + resultText += '\t'; + } + resultText += content; + } + return resultText; +} diff --git a/source/MissionAction.h b/source/MissionAction.h index 467c3d021eb4..f15e91a6bfdb 100644 --- a/source/MissionAction.h +++ b/source/MissionAction.h @@ -16,6 +16,8 @@ this program. If not, see . #ifndef MISSION_ACTION_H_ #define MISSION_ACTION_H_ +#include "ConditionSet.h" +#include "ConditionsStore.h" #include "Conversation.h" #include "ExclusiveItem.h" #include "GameAction.h" @@ -23,8 +25,8 @@ this program. If not, see . #include "Phrase.h" #include -#include #include +#include class DataNode; class DataWriter; @@ -71,18 +73,41 @@ class MissionAction { // "Instantiate" this action by filling in the wildcard text for the actual // destination, payment, cargo, etc. - MissionAction Instantiate(std::map &subs, + MissionAction Instantiate(const ConditionsStore &store, std::map &subs, const System *origin, int jumps, int64_t payload) const; int64_t Payment() const noexcept; + +private: + class MissionDialog { + public: + MissionDialog(const ExclusiveItem &); + MissionDialog(const std::string &); + MissionDialog(const DataNode &); + + + std::string dialogText; + ExclusiveItem dialogPhrase; + ConditionSet condition; + }; + + +private: + std::string CollapseDialog(const ConditionsStore *store, const std::map *subs) const; + + private: std::string trigger; std::string system; LocationFilter systemFilter; + // Dialog text of instantiated missions, or missions with pure-text dialog (no conditions or phrase blocks) std::string dialogText; - ExclusiveItem dialogPhrase; + + // Logic for creating dialog text. Only valid for missions read in from game data files. + std::vector dialog; + ExclusiveItem conversation; // Outfits that are required to be owned (or not) for this action to be performable. diff --git a/source/NPC.cpp b/source/NPC.cpp index 1df57e75fd0f..5d7250df4296 100644 --- a/source/NPC.cpp +++ b/source/NPC.cpp @@ -632,8 +632,8 @@ bool NPC::HasFailed() const // Create a copy of this NPC but with the fleets replaced by the actual // ships they represent, wildcards in the conversation text replaced, etc. -NPC NPC::Instantiate(map &subs, const System *origin, const System *destination, - int jumps, int64_t payload) const +NPC NPC::Instantiate(const ConditionsStore &conditions, map &subs, const System *origin, + const System *destination, int jumps, int64_t payload) const { NPC result; result.government = government; @@ -665,7 +665,7 @@ NPC NPC::Instantiate(map &subs, const System *origin, const Syst return result; } for(const auto &it : npcActions) - result.npcActions[it.first] = it.second.Instantiate(subs, origin, jumps, payload); + result.npcActions[it.first] = it.second.Instantiate(conditions, subs, origin, jumps, payload); // Pick the system for this NPC to start out in. result.system = system; diff --git a/source/NPC.h b/source/NPC.h index 97d74101c6b5..4dfeb9617f79 100644 --- a/source/NPC.h +++ b/source/NPC.h @@ -110,8 +110,8 @@ class NPC { // Create a copy of this NPC but with the fleets replaced by the actual // ships they represent, wildcards in the conversation text replaced, etc. - NPC Instantiate(std::map &subs, const System *origin, const System *destination, - int jumps, int64_t payload) const; + NPC Instantiate(const ConditionsStore &store, std::map &subs, + const System *origin, const System *destination, int jumps, int64_t payload) const; private: diff --git a/source/NPCAction.cpp b/source/NPCAction.cpp index 82ac2e409962..bdb4b2044bb0 100644 --- a/source/NPCAction.cpp +++ b/source/NPCAction.cpp @@ -90,11 +90,11 @@ void NPCAction::Do(PlayerInfo &player, UI *ui, const Mission *caller) // Convert this validated template into a populated action. -NPCAction NPCAction::Instantiate(map &subs, const System *origin, +NPCAction NPCAction::Instantiate(const ConditionsStore &store, map &subs, const System *origin, int jumps, int64_t payload) const { NPCAction result; result.trigger = trigger; - result.action = action.Instantiate(subs, origin, jumps, payload); + result.action = action.Instantiate(store, subs, origin, jumps, payload); return result; } diff --git a/source/NPCAction.h b/source/NPCAction.h index 64fd3afeca36..6376bdfe7d82 100644 --- a/source/NPCAction.h +++ b/source/NPCAction.h @@ -50,7 +50,7 @@ class NPCAction { // "Instantiate" this action by filling in the wildcard text for the actual // destination, payment, cargo, etc. - NPCAction Instantiate(std::map &subs, + NPCAction Instantiate(const ConditionsStore &store, std::map &subs, const System *origin, int jumps, int64_t payload) const; From e330807c71ffecfda0f2549e0302c37118166f91 Mon Sep 17 00:00:00 2001 From: Peter van der Meer Date: Sun, 23 Jun 2024 09:56:34 +0200 Subject: [PATCH 31/75] refactor: Move the String Interner from Dictionary to its own class (#5748) --- source/CMakeLists.txt | 2 + source/Dictionary.cpp | 16 ++---- source/StringInterner.cpp | 52 ++++++++++++++++++ source/StringInterner.h | 35 ++++++++++++ tests/CMakeLists.txt | 1 + tests/unit/src/test_stringInterner.cpp | 73 ++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 source/StringInterner.cpp create mode 100644 source/StringInterner.h create mode 100644 tests/unit/src/test_stringInterner.cpp diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 93c8f9db2575..44f44c8b6188 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -316,6 +316,8 @@ target_sources(EndlessSkyLib PRIVATE StartConditionsPanel.h StellarObject.cpp StellarObject.h + StringInterner.cpp + StringInterner.h System.cpp System.h SystemEntry.h diff --git a/source/Dictionary.cpp b/source/Dictionary.cpp index 70628b5d451a..02eb1b1ba3cb 100644 --- a/source/Dictionary.cpp +++ b/source/Dictionary.cpp @@ -15,6 +15,8 @@ this program. If not, see . #include "Dictionary.h" +#include "StringInterner.h" + #include #include #include @@ -46,18 +48,6 @@ namespace { } return make_pair(low, false); } - - // String interning: return a pointer to a character string that matches the - // given string but has static storage duration. - const char *Intern(const char *key) - { - static set interned; - static mutex m; - - // Just in case this function is accessed from multiple threads: - lock_guard lock(m); - return interned.insert(key).first->c_str(); - } } @@ -68,7 +58,7 @@ double &Dictionary::operator[](const char *key) if(pos.second) return data()[pos.first].second; - return insert(begin() + pos.first, make_pair(Intern(key), 0.))->second; + return insert(begin() + pos.first, make_pair(StringInterner::Intern(key), 0.))->second; } diff --git a/source/StringInterner.cpp b/source/StringInterner.cpp new file mode 100644 index 000000000000..e7855de58fbc --- /dev/null +++ b/source/StringInterner.cpp @@ -0,0 +1,52 @@ +/* StringInterner.cpp +Copyright (c) 2017-2022 by Michael Zahniser + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#include "StringInterner.h" + +#include +#include +#include +#include + +using namespace std; + + + +// String interning: return a pointer to a character string that matches the +// given string but has static storage duration. +const char *StringInterner::Intern(const char *key) +{ + static set interned; + static shared_mutex m; + + // Search using a shared lock, allows parallel access by multiple threads. + { + shared_lock readLock(m); + auto it = interned.find(key); + if(it != interned.end()) + return it->c_str(); + } + + // Insert using an exclusive lock, if needed. Blocks all parallel access. + unique_lock writeLock(m); + return interned.insert(key).first->c_str(); +} + + + +const char *StringInterner::Intern(const string key) +{ + return Intern(key.c_str()); +} diff --git a/source/StringInterner.h b/source/StringInterner.h new file mode 100644 index 000000000000..efc5646b2d18 --- /dev/null +++ b/source/StringInterner.h @@ -0,0 +1,35 @@ +/* StringInterner.h +Copyright (c) 2017-2022 by Michael Zahniser + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#ifndef STRING_INTERNER_H_ +#define STRING_INTERNER_H_ + +#include + + + +// This class stores a set of interned strings. Interning can be a slow operation during string creation/interning, but +// it will allow fast char-pointer based comparisons when comparing two interned strings (because interning ensures that +// each interned string only appears once in the set). Full string compares will still be needed when comparing interned +// strings to non-interned strings. +class StringInterner { +public: + static const char *Intern(const char *key); + static const char *Intern(const std::string key); +}; + + + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 88546e66c816..7b85bfeec60f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,7 @@ target_sources(EndlessSkyTests PRIVATE unit/src/test_scrollVar.cpp unit/src/test_set.cpp unit/src/test_ship.cpp + unit/src/test_stringInterner.cpp unit/src/test_template.txt unit/src/test_weightedList.cpp unit/src/text/test_alignment.cpp diff --git a/tests/unit/src/test_stringInterner.cpp b/tests/unit/src/test_stringInterner.cpp new file mode 100644 index 000000000000..49a542ee8ef9 --- /dev/null +++ b/tests/unit/src/test_stringInterner.cpp @@ -0,0 +1,73 @@ +/* test_stringInterner.cpp +Copyright (c) 2021 by Peter van der Meer + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#include "es-test.hpp" + +// Include only the tested class's header. +#include "../../../source/StringInterner.h" + +// ... and any system includes needed for the test file. + +namespace { // test namespace +// #region mock data +// #endregion mock data + + + +// #region unit tests +SCENARIO( "Interning String", "[StringInterner]" ) { + GIVEN( "An empty start" ) { + std::string ahAh = "ah ah"; + std::string blaBla = "bla bla"; + std::string daDa = "da da"; + const char *blaBlaPtr = nullptr; + + WHEN( "the string is interned" ) { + blaBlaPtr = StringInterner::Intern(blaBla); + THEN( "it still represents the same string" ) { + // This is a string comparison (since blaBla is a string). + CHECK( blaBla == blaBlaPtr ); + } + THEN( "interning twice results in the same char pointer twice" ) { + const char *blaBlaPtr2 = StringInterner::Intern(blaBla); + // This is a pointer comparison (since both arguments are char-pointers). + CHECK( blaBlaPtr2 == blaBlaPtr ); + } + THEN( "interning other strings still results in the same char pointer" ) { + const char *blaBlaPtr3 = StringInterner::Intern(blaBla); + // This is a pointer comparison (since both arguments are char-pointers). + CHECK( blaBlaPtr3 == blaBlaPtr ); + StringInterner::Intern(ahAh); + StringInterner::Intern(daDa); + const char *blaBlaPtr4 = StringInterner::Intern(blaBla); + // This is a pointer comparison (since both arguments are char-pointers). + CHECK( blaBlaPtr4 == blaBlaPtr ); + // Those are string comparisons (since blaBla is a string). + CHECK( blaBla == blaBlaPtr4 ); + } + THEN( "interning another string results in another pointer" ) + { + const char *daDaPtr = StringInterner::Intern(daDa); + // This is a pointer comparison (since both arguments are char-pointers). + CHECK( blaBlaPtr != daDaPtr ); + } + } + } +} +// #endregion unit tests + + + +} // test namespace From 546dd7a0077e1bc57d7fd2c9ba5d947f3f10565d Mon Sep 17 00:00:00 2001 From: Nicholas Shanks Date: Sun, 23 Jun 2024 09:02:03 +0100 Subject: [PATCH 32/75] feat(ui): Add parked label to the ship tooltip. (#10197) --- data/_ui/tooltips.txt | 6 ++++++ source/ShopPanel.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/data/_ui/tooltips.txt b/data/_ui/tooltips.txt index 92f5e68fe33d..3dca41176a59 100644 --- a/data/_ui/tooltips.txt +++ b/data/_ui/tooltips.txt @@ -11,6 +11,12 @@ # You should have received a copy of the GNU General Public License along with # this program. If not, see . +# Fleet management: +tip "parked" + `(parked)` + + + # Outfit and ship attributes: tip "acceleration:" `How quickly this ship gains speed. The higher a ship's mass (including the mass of any cargo or fighters it is carrying), the slower it accelerates.` diff --git a/source/ShopPanel.cpp b/source/ShopPanel.cpp index 31293d6fdd62..e2215205f4ca 100644 --- a/source/ShopPanel.cpp +++ b/source/ShopPanel.cpp @@ -773,7 +773,7 @@ void ShopPanel::DrawShipsSidebar() if(mouse.Y() < Screen::Bottom() - BUTTON_HEIGHT && shipZones.back().Contains(mouse)) { - shipName = ship->Name(); + shipName = ship->Name() + (ship->IsParked() ? "\n" + GameData::Tooltip("parked") : ""); hoverPoint = shipZones.back().TopLeft(); } From 67e3f4cccd4f3e0b62bcfa118d3a77ec81bd68b7 Mon Sep 17 00:00:00 2001 From: Nicholas Shanks Date: Sun, 23 Jun 2024 09:04:24 +0100 Subject: [PATCH 33/75] feat(ui): Ship parked indicator in shipyards and outfitters (#10195) --- source/ShopPanel.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/ShopPanel.cpp b/source/ShopPanel.cpp index e2215205f4ca..4e52e09295de 100644 --- a/source/ShopPanel.cpp +++ b/source/ShopPanel.cpp @@ -692,6 +692,7 @@ const Outfit *ShopPanel::Zone::GetOutfit() const void ShopPanel::DrawShipsSidebar() { const Font &font = FontSet::Get(14); + const Color &dark = *GameData::Colors().Get("dark"); const Color &medium = *GameData::Colors().Get("medium"); const Color &bright = *GameData::Colors().Get("bright"); @@ -791,6 +792,13 @@ void ShopPanel::DrawShipsSidebar() PointerShader::Draw(Point(point.X() - static_cast(ICON_TILE / 3), point.Y()), Point(1., 0.), 14.f, 12.f, 0., Color(.9f, .9f, .9f, .2f)); + if(ship->IsParked()) + { + static const Point CORNER = .35 * Point(ICON_TILE, ICON_TILE); + FillShader::Fill(point + CORNER, Point(6., 6.), dark); + FillShader::Fill(point + CORNER, Point(4., 4.), isSelected ? bright : medium); + } + point.X() += ICON_TILE; } point.Y() += ICON_TILE; From 7e5f7afa935e1229c31d3c69e1ccccdf23dfe049 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Sun, 23 Jun 2024 11:07:56 +0300 Subject: [PATCH 34/75] feat(content): More Commodities (#10179) --- data/commodities.txt | 195 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 185 insertions(+), 10 deletions(-) diff --git a/data/commodities.txt b/data/commodities.txt index d0535bb4809a..7190351b5c62 100644 --- a/data/commodities.txt +++ b/data/commodities.txt @@ -13,21 +13,52 @@ trade commodity "Food" 100 600 + "acorns" + "acorn bread" + "alfalfa" + "alfredo sauce" + "alligator meat" + "almond butter" + "almond milk" + "almonds" "animal feed" + "antelopes" + "antelope meat" + "apple juice" + "apple pie" "apples" "applesauce" + "apricots" + "artichokes" + "asparagus" "avocados" + "bacon" + "baking powder" + "baking soda" "bananas" + "banana bread" "barley" + "basil" + "beans" + "beef" "beer" + "beets" + "bell peppers" "black beans" + "blackberries" + "blueberries" "bouillon cubes" "boxed lunches" "bread" "breakfast cereal" + "brisket" + "broccoli" "brownie mix" + "brown rice" + "buns" "butter" "cake mix" + "calcium supplements" "candy" "canned anchovies" "canned artichokes" @@ -37,19 +68,28 @@ trade "canned olives" "canned soup" "canned tuna" + "cantaloupes" "cattle" "cheese" + "cherries" + "chicken" "chocolate" + "chewing gum" "cloned meat" + "coconuts" + "coconut oil" "coffee beans" "cookies" "cooking oils" "corn oil" "corn" + "courgettes" "crackers" "croutons" + "cucumbers" "cured meat" "dairy products" + "dates" "dried apples" "dried apricots" "dried meat" @@ -58,6 +98,10 @@ trade "evaporated milk" "fast food supplies" "fertilizer" + "fettucini" + "flapjacks" + "flour" + "French toast" "frozen dumplings" "frozen fish" "frozen meat" @@ -66,25 +110,50 @@ trade "fruit juice" "garlic" "grain" + "granola" + "grapes" + "green beans" "guavas" + "gum" + "hamburgers" "honey" "hops" + "hot dogs" + "horse meat" "jerky" + "kiwis" "lard" + "lemons" "lentils" + "lettuce" + "maize" "mangoes" "maple syrup" + "melons" + "milk" + "millet" "molasses" + "noodles" + "oatmeal" "oats" "olive oil" + "olives" "onions" + "oranges" + "pancakes" + "parsley" + "pasta" "peanut oil" + "pears" "pepper" "pickles" "pineapples" "pinto beans" "popcorn kernels" + "popsicles" + "pork" "potatoes" + "powdered sugar" "powdered toast" "pretzels" "protein bars" @@ -95,7 +164,10 @@ trade "raisins" "ramen" "ready-to-eat meals" + "red beans" "rice" + "rye" + "rye bread" "salt" "seaweed" "sheep" @@ -105,22 +177,41 @@ trade "soy products" "spices" "split peas" + "strawberries" + "strawberry shortcakes" "sugar" + "sugar beets" + "sugar cane" + "sunflower oil" + "sunflower seeds" + "syrup" "tea" "teff" + "toast" "tortillas" "trail mix" + "turkeys" + "turkey meat" "vanilla" + "venison" "vinegar" "vitamin pills" + "waffles" + "watermelons" "wheat" + "white rice" "yams" "yeast extract" "yogurt" + "zucchini" + "zucchini bread" commodity "Clothing" 140 440 "athletic wear" + "balaclavas" "ball gowns" "balls of yarn" + "baseball hats" + "bathrobes" "beach towels" "belts" "blankets" @@ -165,6 +256,7 @@ trade "helmets" "insulated fabric" "jackets" + "jeans" "khakis" "kimonos" "life jackets" @@ -185,6 +277,7 @@ trade "rain boots" "raincoats" "rayon" + "rings" "robes" "running shoes" "sailcloth" @@ -193,6 +286,7 @@ trade "scarves" "shawls" "shirts" + "shoes" "shoelaces" "skirts" "slippers" @@ -205,6 +299,7 @@ trade "sweaters" "sweatpants" "swimsuits" + "swimwear" "tank tops" "textiles" "ties" @@ -216,6 +311,7 @@ trade "umbrellas" "underwear" "uniforms" + "vests" "wallets" "watches" "waterproof fiber" @@ -248,6 +344,7 @@ trade "galvanized steel" "indium" "indium tin oxide" + "iron" "manganese" "metallic foam" "magnesium" @@ -261,6 +358,7 @@ trade "platinum wool" "platinum" "rebar" + "rusted iron" "scandium" "sheet metal" "silicon bronze" @@ -287,6 +385,7 @@ trade "dacron" "diesel oil" "epoxy resin" + "filament" "gasoline" "high-tensile fibers" "jet fuel" @@ -299,10 +398,11 @@ trade "paint" "petrochemicals" "petroleum" + "plastics" "plastic pellets" - "plastic welding rod" + "plastic welding rods" "plastic wrap" - "plumber's pipe" + "plumber's pipes" "polyethylene" "polymerizing agents" "polystyrene foam" @@ -323,8 +423,10 @@ trade "alarm systems" "bicycles" "breadmaking machines" + "calculators" "cameras" "cargo containers" + "computers" "concrete mixers" "collimators" "combustion engines" @@ -336,8 +438,10 @@ trade "fencing" "floodlights" "flow meters" + "flying cars" "freezers" "fuel tanks" + "hand carts" "harvesters" "hoverskates" "humidifiers" @@ -353,7 +457,9 @@ trade "ovens" "pallets" "particle analyzers" + "personal devices" "power tools" + "printheads" "radar arrays" "razor blades" "refrigerators" @@ -365,17 +471,20 @@ trade "spacecraft hulls" "steam engines" "sump pumps" + "tablets" "tavern fixtures" "theatrical props" "toilets" "tools" "tractor parts" "transits" + "trolleys" "turbines" "valves" "vehicle chassis" "vehicles" "ventilators" + "watches" "water heaters" "water purifiers" "water tanks" @@ -404,6 +513,7 @@ trade "coffins" "colposcopes" "cough drops" + "crutches" "cytometers" "defibrillators" "dentures" @@ -416,6 +526,8 @@ trade "first aid kits" "forceps" "gauze pads" + "gloves" + "hairnets" "hospital beds" "hydrogen peroxide" "intravenous fluids" @@ -434,6 +546,7 @@ trade "painkillers" "pharmaceuticals" "physical therapy equipment" + "pills" "pipettes" "pipette tips" "privacy curtains" @@ -445,11 +558,14 @@ trade "stethoscopes" "surgical equipment" "surgical robots" + "surgical tables" + "syringes" "tissues" "tongue depressors" "tourniquets" "tranquilizers" "tweezers" + "wheelchairs" "X-ray machines" commodity "Industrial" 520 920 "aniline dyes" @@ -468,6 +584,7 @@ trade "coolant" "degreaser fluid" "diamond grit" + "drills" "drive shafts" "electrical wire" "electromagnets" @@ -475,10 +592,12 @@ trade "enamel ovens" "expanding foam" "fiberglass" + "glass" "gravel" "heat exchangers" "hydrochloric acid" "industrial catalysts" + "industrial diamonds" "industrial lasers" "industrial solvents" "insecticides" @@ -522,6 +641,7 @@ trade "turret lathes" "vacuum cleaners" "varnish" + "wood" commodity "Electronics" 590 890 "accelerometers" "amplifiers" @@ -532,23 +652,28 @@ trade "cloud chambers" "conic cegrameters" "cooling fans" + "data chips" "data storage modules" "de-ionizers" "diodes" "display screens" "electrical transformers" + "electronics" "fiber optic cable" "framulators" "fuel cells" "haptic materials" + "hard drives" "heat sinks" "hygrometers" "integrated circuits" "inverters" "ionizers" "light emitting diodes" + "memory cards" "memory modules" "microchips" + "microcontrollers" "microswitches" "microtransistors" "microwave emitters" @@ -583,9 +708,12 @@ trade "trackpads" "transceivers" "tradometers" + "transistors" "vacuum tubes" + "video cards" "voltage converters" "wireless receivers" + "wires" commodity "Heavy Metals" 610 1310 "actinide metals" "barium" @@ -600,6 +728,7 @@ trade "mercury" "neodymium" "neptunium" + "platinum" "plutonium" "polonium" "raw pitchblende" @@ -608,6 +737,9 @@ trade "thorium" "tungsten" commodity "Luxury Goods" 920 1520 + "agates" + "amber" + "amethysts" "antique swords" "bawdy magazines" "brandy" @@ -622,7 +754,9 @@ trade "custom pens" "designer clothing" "designer suits" + "diamonds" "dueling sabers" + "emeralds" "exotic carpets" "exotic hardwoods" "fine art" @@ -634,6 +768,7 @@ trade "handbags" "high-performance automobiles" "hoverskis" + "jade" "jewelry" "lawn furniture" "leather" @@ -645,16 +780,20 @@ trade "monogrammed handkerchiefs" "natural diamonds" "numismatic folders" + "opals" "orchids" "paintings" "paragliders" + "pearls" "perfume" "pleasure craft" "pocket watches" "porcelain" "replica Ming vases" "replica tribal masks" + "rubies" "saffron" + "sapphires" "sculptures" "signed memorabilia" "silk" @@ -709,30 +848,43 @@ trade "volatile gases" "waste" commodity "Construction" + "bolts" "box girders" "carbon steel" + "cement" "composite beams" "composite fibers" "concrete" + "drills" "duct tape" "fiberglass" + "French windows" "girders" + "gravel" "I-beams" + "jackhammers" "joint plates" "lumber" "metal joists" "metal trusses" + "nails" + "nuts and bolts" "pylons" "rivets" "rolled steel" "roofing" + "screws" "sheet metal" + "steel" "steel beams" "steel bolts" "steel struts" "structural steel" "tee joints" + "tiles" "welding rods" + "windows" + "wood" commodity "Illegal Substances" "bad blues" "boosters" @@ -744,11 +896,11 @@ trade "hallucinogens" "horse tranquilizers" "memory chips" - "star shrooms" "quasars" "red giants" "southern snacks" "space weed" + "star shrooms" "steam" "steroids" "stolen pharmaceuticals" @@ -856,6 +1008,7 @@ trade "outlawed pesticides" "pathogen samples" "plundered artifacts" + "Quarg technology" "remote detonators" "reprogrammed companion robots" "stolen military secrets" @@ -867,30 +1020,52 @@ trade "weaponized chickens" "weapons-grade uranium" commodity "void sprite parts" + "dismembered void sprites" "void sprite parts" "void sprite limbs" - "dismembered void sprites" commodity "Military" "ammunition" "armor plating" + "armored cars" + "artillery" + "atmospheric bombers" "atmospheric craft" "battlefield drones" + "bunker parts" "camouflage fatigues" "combat vehicles" + "explosives" "field rations" + "fighter jets" "firearms" + "flamethrowers" + "grenades" "guided bombs" + "guided missiles" + "guns" + "hand grenades" + "heat detectors" + "howitzers" + "machine guns" + "mines" + "missiles" "mortars" "munitions" + "orbital bombers" "painkillers" + "pistols" "portable bunkers" "radar arrays" + "rail guns" "rifles" "shells" "stretchers" + "submarines" "surface-to-air missiles" + "tanks" "targeting systems" "tents" + "transport vehicles" commodity "Ship Systems" "auxiliary ramscoops" "bulkhead control systems" @@ -937,13 +1112,14 @@ trade "titanium ship plating" commodity "frozen aid" "blankets" - "heaters" "emergency fuel" + "field rations" + "firewood" + "generators" + "heaters" + "mittens" "parkas" "tuques" - "mittens" - "generators" - "field rations" commodity "flames aid" "air quality detectors" "airborne fire suppressant drones" @@ -953,8 +1129,7 @@ trade "fire retardant" "firehoses" "flame-resistant balaclavas" + "sand" "seed packets" "tents" "trenching robots" - - From d57045140c98851a903d5d13a5b2b0a088d7acd7 Mon Sep 17 00:00:00 2001 From: Unordered Sigh <116329264+UnorderedSigh@users.noreply.github.com> Date: Sun, 23 Jun 2024 04:27:23 -0400 Subject: [PATCH 35/75] feat(enhancement): Expand phrases and substitutions in custom substitutions (#8283) --- source/Mission.cpp | 10 ++++ source/text/Format.cpp | 127 +++++++++++++++++++++++++++++++---------- source/text/Format.h | 3 + 3 files changed, 110 insertions(+), 30 deletions(-) diff --git a/source/Mission.cpp b/source/Mission.cpp index 795b0f08bc21..7d758f56e908 100644 --- a/source/Mission.cpp +++ b/source/Mission.cpp @@ -24,6 +24,7 @@ this program. If not, see . #include "Government.h" #include "Logger.h" #include "Messages.h" +#include "Phrase.h" #include "Planet.h" #include "PlayerInfo.h" #include "Random.h" @@ -1006,6 +1007,10 @@ string Mission::BlockedMessage(const PlayerInfo &player) out << "no additional space"; subs[""] = out.str(); + for(const auto &keyValue : subs) + subs[keyValue.first] = Phrase::ExpandPhrases(keyValue.second); + Format::Expand(subs); + string message = Format::Replace(blocked, subs); blocked.clear(); return message; @@ -1470,6 +1475,11 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr &b if(!result.markedSystems.empty()) subs[""] = systemsReplacement(result.markedSystems); + // Done making subs, so expand the phrases and recursively substitute. + for(const auto &keyValue : subs) + subs[keyValue.first] = Phrase::ExpandPhrases(keyValue.second); + Format::Expand(subs); + // Instantiate the NPCs. This also fills in the "" substitution. string reason; for(auto &&n : npcs) diff --git a/source/text/Format.cpp b/source/text/Format.cpp index e3c60730ccd9..0c7dc6761fac 100644 --- a/source/text/Format.cpp +++ b/source/text/Format.cpp @@ -19,7 +19,10 @@ this program. If not, see . #include #include #include +#include +#include #include +#include using namespace std; @@ -134,6 +137,86 @@ namespace { reverse(result.begin(), result.end()); } + string StringSubstituter(const string &source, + function SubstitutionFor) + { + string target; + target.reserve(source.length()); + + string key; + size_t start = 0; + size_t search = start; + while(search < source.length()) + { + size_t left = source.find('<', search); + if(left == string::npos) + break; + + size_t right = source.find('>', left); + if(right == string::npos) + break; + + ++right; + size_t length = right - left; + key.assign(source, left, length); + const string *sub = SubstitutionFor(key); + if(sub) + { + target.append(source, start, left - start); + target.append(*sub, 0, string::npos); + start = right; + search = start; + } + else + search = left + 1; + } + + target.append(source, start, source.length() - start); + return target; + } + + // Helper function for Format::Expand, to recursively expand one key, + // detecting cycles in the graph (and thus avoiding infinite recursion). + void ExpandInto(const string &key, const string &oldValue, const map &source, + map &result, unordered_set &keysBeingExpanded) + { + // Optimization for a common case: no substitutions in the substitution. + if(oldValue.find('<') == string::npos) + { + result.emplace(key, oldValue); + return; + } + + // Declare our intention to process this key so a later attempt will + // detect recursion. + auto inserted = keysBeingExpanded.insert(key); + + auto SubstitutionFor = [&](const string &request) -> const string * + { + auto hasResult = result.find(request); + // Already finished this one. + if(hasResult != result.end()) + return &hasResult->second; + // Refuse to traverse a cycle in the graph. + if(keysBeingExpanded.find(request) != keysBeingExpanded.end()) + return nullptr; + auto hasSource = source.find(request); + // Undefined key. + if(hasSource == source.end()) + return nullptr; + // This key-value pair has not been expanded yet. + ExpandInto(request, hasSource->second, source, result, keysBeingExpanded); + hasResult = result.find(request); + return hasResult == result.end() ? nullptr : &hasResult->second; + }; + + string newValue = StringSubstituter(oldValue, SubstitutionFor); + + // Success! Indicate we're done expanding this key, and provide its value. + keysBeingExpanded.erase(inserted.first); + result.emplace(key, newValue); + } + // Helper function for ExpandConditions. // // source.substr(formatStart, formatSize) contains the format (credits, mass, etc.) @@ -485,41 +568,25 @@ double Format::Parse(const string &str) string Format::Replace(const string &source, const map &keys) { - string result; - result.reserve(source.length()); - - size_t start = 0; - size_t search = start; - while(search < source.length()) + auto SubstitutionFor = [&](const string &key) -> const string * { - size_t left = source.find('<', search); - if(left == string::npos) - break; + auto found = keys.find(key); + return (found == keys.end()) ? nullptr : &found->second; + }; - size_t right = source.find('>', left); - if(right == string::npos) - break; + return StringSubstituter(source, SubstitutionFor); +} - bool matched = false; - ++right; - size_t length = right - left; - for(const auto &it : keys) - if(!source.compare(left, length, it.first)) - { - result.append(source, start, left - start); - result.append(it.second); - start = right; - search = start; - matched = true; - break; - } - if(!matched) - search = left + 1; - } - result.append(source, start, source.length() - start); - return result; +void Format::Expand(map &keys) +{ + map newKeys; + unordered_set keysBeingExpanded; + for(auto it = keys.begin(); it != keys.end(); ++it) + if(newKeys.find(it->first) == newKeys.end()) + ExpandInto(it->first, it->second, keys, newKeys, keysBeingExpanded); + keys.swap(newKeys); } diff --git a/source/text/Format.h b/source/text/Format.h index 7e030be02fb3..eb0d94b90a8c 100644 --- a/source/text/Format.h +++ b/source/text/Format.h @@ -66,6 +66,9 @@ class Format { // Replace a set of "keys," which must be strings in the form "", with // a new set of strings, and return the result. static std::string Replace(const std::string &source, const std::map &keys); + // Recursively expand substitutions in all key/value pairs. Will detect + // infinite recursion; offending substitutions will not be expanded. + static void Expand(std::map &keys); // Replace all occurrences of "target" with "replacement" in-place. static void ReplaceAll(std::string &text, const std::string &target, const std::string &replacement); From 18eb4a3a41a59d9334a1a364e2b73dc3421b41e6 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Sun, 23 Jun 2024 11:31:46 +0300 Subject: [PATCH 36/75] feat(content): Add tribute fleets to Wanderer worlds (#10221) --- data/map planets.txt | 54 ++++++++++++++++++++++++++++++++++++- data/wanderer/wanderers.txt | 36 ++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/data/map planets.txt b/data/map planets.txt index 009b7832bea8..397568b17f3e 100644 --- a/data/map planets.txt +++ b/data/map planets.txt @@ -2449,6 +2449,9 @@ planet "Kort Kehai" spaceport ` The hallway that loops around the outside of the garden is decorated with an incredibly detailed mural. At one end the mural depicts a barren and blasted landscape, which gradually transitions along the length of the hallway to a green and growing world. The mural must be a reminder to the workers of what they are striving to accomplish here.` "required reputation" 2 bribe 0 + tribute 100 + threshold 1500 + fleet "Wanderer Defense" 3 planet "Kort Vek'kri" attributes wanderer @@ -2457,6 +2460,9 @@ planet "Kort Vek'kri" spaceport `This small spaceport is unmistakably a research station, and the Wanderers walking or flying from one building to another have a self-absorbed attitude not unlike what you would expect from human scientists. Around the outskirts of the port village are pens that hold a staggering variety of animals, mostly ruminants and other herbivores, and the work of the scientists seems to revolve around convincing these animals to eat the local flora.` "required reputation" 2 bribe 0 + tribute 500 + threshold 2000 + fleet "Wanderer Defense" 4 planet "Kraken Station" attributes "near earth" station @@ -4495,10 +4501,13 @@ planet "Tik Klai" attributes research wanderer landscape land/badlands2 description `This world is uncomfortably hot, even for the Wanderers, and so close to its star that the surface is bathed in dangerous levels of ultraviolet light. The few Wanderer installations here are buried underground, with only a few access hatches and storage buildings on the surface.` - spaceport `Most of the spaceport facility is closed to non-Wanderer personnel, and apparently to many of the off-world Wanderers who are visiting, as well. The underground passageways are comfortably wide and tall for a human, but the wanderers walking through them must tuck their wings in close to their bodies to avoid scraping against the ceilings.` + spaceport `Most of the spaceport facility is closed to non-Wanderer personnel, and apparently to many of the off-world Wanderers who are visiting, as well. The underground passageways are comfortably wide and tall for a human, but the Wanderers walking through them must tuck their wings in close to their bodies to avoid scraping against the ceilings.` spaceport ` The warning signs marking restricted areas are in the undecipherable Wanderer language, but accompanied by a symbol you easily recognize: a DNA double helix undergoing replication.` "required reputation" 35 bribe 0 + tribute 600 + threshold 3500 + fleet "Wanderer Defense" 4 planet Tinker attributes factory mining "near earth" @@ -4688,6 +4697,9 @@ planet "Var' Kar'i'i" outfitter "Wanderer Basics" "required reputation" 5 bribe 0 + tribute 1700 + threshold 5000 + fleet "Wanderer Defense" 7 planet "Var' Kayi" attributes urban wanderer @@ -4699,6 +4711,9 @@ planet "Var' Kayi" outfitter "Wanderer Advanced" "required reputation" 5 bribe 0 + tribute 4500 + threshold 12500 + fleet "Wanderer Defense" 12 planet "Var' Roi" attributes frozen moon research wanderer @@ -4707,6 +4722,9 @@ planet "Var' Roi" spaceport `The only habitation on this moon is a Wanderer research station suspended on metal pylons well above the icy surface of the ocean. The gravity here is low enough that even though the atmosphere is thin, the Wanderers who work in the station can fly out across the ocean on their own power, wearing some sort of breathing gear. The researchers also have a fleet of submarine ships, sturdy enough to pierce through the shifting ice and travel deep below the ocean surface.` "required reputation" 25 bribe 0 + tribute 100 + threshold 1500 + fleet "Wanderer Defense" 4 planet "Vara K'chrai" attributes urban wanderer @@ -4717,6 +4735,9 @@ planet "Vara K'chrai" shipyard "Wanderer Advanced" outfitter "Wanderer Advanced" bribe 0 + tribute 8000 + threshold 14000 + fleet "Wanderer Defense" 17 planet "Vara Ke'sok" attributes fishing wanderer @@ -4725,6 +4746,9 @@ planet "Vara Ke'sok" spaceport `The raft of algae that supports the spaceport is probably at least a dozen meters thick, but flexible enough that it bends as the ocean swells pass underneath, causing individual buildings to rise up or tilt slightly relative to their neighbors. The effect is subtle, but disconcerting, and you cannot help but stumble drunkenly at times as you explore the island.` "required reputation" 5 bribe 0 + tribute 500 + threshold 2500 + fleet "Wanderer Defense" 4 planet "Vara Ke'stai" attributes farming wanderer @@ -4734,6 +4758,9 @@ planet "Vara Ke'stai" outfitter "Wanderer Basics" "required reputation" 5 bribe 0 + tribute 650 + threshold 4300 + fleet "Wanderer Defense" 3 planet "Vara Kehi'ki" attributes farming research wanderer @@ -4743,6 +4770,9 @@ planet "Vara Kehi'ki" outfitter "Wanderer Basics" "required reputation" 15 bribe 0 + tribute 900 + threshold 3500 + fleet "Wanderer Defense" 5 planet "Vara Pug" attributes pug @@ -4763,6 +4793,9 @@ planet "Vara Rakak" outfitter "Wanderer Advanced" "required reputation" 5 bribe 0 + tribute 2300 + threshold 6500 + fleet "Wanderer Defense" 5 planet "Varu Ek'lak'lai" attributes mining wanderer @@ -4773,6 +4806,9 @@ planet "Varu Ek'lak'lai" outfitter "Wanderer Basics" "required reputation" 15 bribe 0 + tribute 1300 + threshold 3400 + fleet "Wanderer Defense" 4 planet "Varu K'est" attributes military wanderer @@ -4783,6 +4819,9 @@ planet "Varu K'est" outfitter "Wanderer Advanced" "required reputation" 15 bribe 0 + tribute 1600 + threshold 5500 + fleet "Wanderer Defense" 16 planet "Varu K'prai" attributes oil wanderer @@ -4792,6 +4831,10 @@ planet "Varu K'prai" outfitter "Wanderer Basics" "required reputation" 15 bribe 0 + tribute 800 + threshold 1800 + fleet "Wanderer Defense" 2 + fleet "Wanderer Freight" 4 planet "Varu Mer'ek" attributes factory wanderer @@ -4802,6 +4845,9 @@ planet "Varu Mer'ek" outfitter "Wanderer Advanced" "required reputation" 15 bribe 0 + tribute 1600 + threshold 3200 + fleet "Wanderer Defense" 3 planet "Varu Tek'kai" attributes factory moon wanderer @@ -4811,6 +4857,9 @@ planet "Varu Tek'kai" outfitter "Wanderer Basics" "required reputation" 15 bribe 0 + tribute 200 + threshold 1700 + fleet "Wanderer Defense" 4 planet "Varu Tev'kei" attributes factory wanderer @@ -4820,6 +4869,9 @@ planet "Varu Tev'kei" outfitter "Wanderer Basics" "required reputation" 15 bribe 0 + tribute 500 + threshold 1950 + fleet "Wanderer Defense" 5 planet Vatican attributes lava uninhabited diff --git a/data/wanderer/wanderers.txt b/data/wanderer/wanderers.txt index 32233569f657..705c326d4e6d 100644 --- a/data/wanderer/wanderers.txt +++ b/data/wanderer/wanderers.txt @@ -779,6 +779,7 @@ event "wanderers: unfettered invasion starts" spaceport `The Unfettered are using the few jump drives they have to ferry more and more of their people into Wanderer space. When each wave of ships arrives here in the spaceport, technicians carefully remove the jump drives from all but a few of them. A few well guarded jump-capable ships then carry those spare jump drives back to Unfettered space, where they are used to bring more ships and people.` shipyard clear outfitter clear + tribute clear planet "Varu Mer'ek" add attributes "evacuation" @@ -865,12 +866,14 @@ event "wanderers: more systems lost" description ` Now that the Unfettered have taken over control of the planet, it may be only a matter of time before the deserts dominate it once again.` spaceport `The spaceport is in a forest village near the planet's north pole. The landing pads are massive stone monoliths laid on the ground in a nearby clearing. But aside from a few large warehouses at ground level, the village itself is entirely made of tree-houses. Although they are very different from Hai architecture, the Unfettered seem to be quite at home living in the tree-houses, and have not built many of their own structures yet.` outfitter clear + tribute clear planet "Var' Kar'i'i" attributes "unfettered" "urban" "farming" description `Of all the worlds that the Unfettered have captured, this is surely the one that the Wanderers will miss the most: a heavily forested planet that was once home to countless small villages and even a few cities. The Unfettered are clearly doing their best to tend the farms here, but they operate on a permaculture system that relies heavily on maintaining a delicate ecological balance, rather than on machines and pesticides.` spaceport `A large number of Unfettered settlers have already arrived here and begun setting up a spaceport village, complete with markets, taverns, and military barracks. To the Unfettered, who have lived their whole lives on worlds where the local ecology is in shambles, this planet must seem like a paradise.` - shipyard clear outfitter clear + shipyard clear + tribute clear planet "Vara Ke'sok" attributes "unfettered" "fishing" description `The surface of this world is almost entirely ocean. The Wanderer settlements were built on massive floating algae mats, some of them the size of a small city. Engines are attached to some of these floating villages, allowing them to be slowly propelled from one part of the planet's surface to another, and the only native industries are fishing and seaweed farming.` @@ -981,6 +984,9 @@ event "wanderers: spera anatrusk colony" spaceport `The Wanderers have hollowed out hangars and caves in the soft sandstone walls of this canyon and have begun to build a military base where they can store supplies and repair their ships while trying to decide how best to deal with the new challenges that face them in Korath space.` spaceport ` Oddly enough, they have also found enough time to plant a large garden on a terrace near the base of the canyon, where they are experimenting to find out which plant species will best grow in this arid environment. The Wanderer drive to understand and transform the local ecology is strong, indeed.` outfitter "Wanderer Basics" + tribute 600 + threshold 1200 + fleet "Wanderer Defense" 2 event "wanderers: hurricane mass production" fleet "Wanderer Defense" @@ -1015,6 +1021,9 @@ event "wanderers: desi seledrak" description `Plumes of soot and ash rise from a cluster of several dozen volcanoes on this planet's main continent, shrouding the planet in a thick layer of clouds and choking out the sunlight. The ruins of several Korath cities and the geometric grids of cleared land surrounding them show that this was once a farming world, but now few plants are able to survive and kilometer-high glaciers have begun creeping down from the poles.` description ` With some help from the Mereti drones, the Wanderers have set up factories here that are pumping out carbon dioxide and methane to warm the atmosphere. In a few decades, the glaciers may start to recede.` spaceport `This odd settlement was built half by the Wanderers and half by the Mereti drones. The layout of the streets is completely chaotic, and they have planted rows of trees and bushes everywhere: some inside greenhouses, and some hardier species out in the open air. The settlement is near the equator, but it is still not particularly warm.` + tribute 500 + threshold 1100 + fleet "Wanderer Defense" 2 event "wanderers: moonbeam mass production" fleet "Wanderer Defense" @@ -1036,3 +1045,28 @@ event "wanderers: moonbeam mass production" outfitter "Wanderer Advanced" "Moonbeam" "Moonbeam Turret" + +event "wanderer tribute patch" + planet "Varu K'est" + tribute clear + planet "Vara Ke'stai" + tribute clear + planet "Var' Kar'i'i" + tribute clear + planet "Spera Anatrusk" + tribute 600 + threshold 1200 + fleet "Wanderer Defense" 2 + planet "Desi Seledrak" + tribute 500 + threshold 1100 + fleet "Wanderer Defense" 2 + +mission "Wanderer Tribute Patch" + invisible + landing + to offer + has "event: wanderers: desi seledrak" + on offer + event "wanderer tribute patch" + fail From 70130c72d28456114e884dba22af59f63857fd1a Mon Sep 17 00:00:00 2001 From: Unordered Sigh <116329264+UnorderedSigh@users.noreply.github.com> Date: Sun, 23 Jun 2024 04:42:06 -0400 Subject: [PATCH 37/75] feat(enhancement): `to display` in planet and spaceport descriptions (#8187) --- source/CMakeLists.txt | 2 + source/MapDetailPanel.cpp | 4 +- source/Paragraphs.cpp | 84 +++++++++++++++++++++++++++++++++++++++ source/Paragraphs.h | 66 ++++++++++++++++++++++++++++++ source/Planet.cpp | 19 ++++----- source/Planet.h | 7 ++-- source/PlanetPanel.cpp | 2 +- source/Port.cpp | 22 +++++----- source/Port.h | 10 +++-- source/PrintData.cpp | 8 +++- source/SpaceportPanel.cpp | 2 +- source/Wormhole.cpp | 2 +- 12 files changed, 190 insertions(+), 38 deletions(-) create mode 100644 source/Paragraphs.cpp create mode 100644 source/Paragraphs.h diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 44f44c8b6188..336f63aa2ed7 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -226,6 +226,8 @@ target_sources(EndlessSkyLib PRIVATE OutlineShader.h Panel.cpp Panel.h + Paragraphs.cpp + Paragraphs.h Person.cpp Person.h Personality.cpp diff --git a/source/MapDetailPanel.cpp b/source/MapDetailPanel.cpp index 6fc0da6f98f2..81976091a4e0 100644 --- a/source/MapDetailPanel.cpp +++ b/source/MapDetailPanel.cpp @@ -819,7 +819,7 @@ void MapDetailPanel::DrawInfo() uiPoint.Y() += 20.; } - if(selectedPlanet && !selectedPlanet->Description().empty() + if(selectedPlanet && !selectedPlanet->Description().IsEmptyFor(player.Conditions()) && player.HasVisited(*selectedPlanet) && !selectedPlanet->IsWormhole()) { static const int X_OFFSET = 240; @@ -832,7 +832,7 @@ void MapDetailPanel::DrawInfo() WrappedText text(font); text.SetAlignment(Alignment::JUSTIFIED); text.SetWrapWidth(WIDTH - 20); - text.Wrap(selectedPlanet->Description()); + text.Wrap(selectedPlanet->Description().ToString(player.Conditions())); text.Draw(Point(Screen::Right() - X_OFFSET - WIDTH, Screen::Top() + 20), medium); selectedSystemOffset = -150; diff --git a/source/Paragraphs.cpp b/source/Paragraphs.cpp new file mode 100644 index 000000000000..9d57672f7497 --- /dev/null +++ b/source/Paragraphs.cpp @@ -0,0 +1,84 @@ +/* Paragraphs.cpp +Copyright (c) 2024 by an anonymous author + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#include "Paragraphs.h" + +#include "ConditionSet.h" +#include "ConditionsStore.h" +#include "DataNode.h" + +using namespace std; + + + +void Paragraphs::Load(const DataNode &node) +{ + for(const DataNode &child : node) + if(child.Size() == 2 && child.Token(0) == "to" && child.Token(1) == "display") + { + text.emplace_back(child, node.Token(1) + "\n"); + return; + } + text.emplace_back(ConditionSet(), node.Token(1) + "\n"); +} + + + +void Paragraphs::Clear() +{ + text.clear(); +} + + + +bool Paragraphs::IsEmpty() const +{ + return text.empty(); +} + + + +bool Paragraphs::IsEmptyFor(const ConditionsStore &vars) const +{ + for(const auto &varsText : text) + if(!varsText.second.empty() && (varsText.first.IsEmpty() || varsText.first.Test(vars))) + return false; + return true; +} + + + +string Paragraphs::ToString(const ConditionsStore &vars) const +{ + string result; + for(const auto &varsText : text) + if(!varsText.second.empty() && (varsText.first.IsEmpty() || varsText.first.Test(vars))) + result += varsText.second; + return result; +} + + + +Paragraphs::ConstIterator Paragraphs::begin() const +{ + return text.begin(); +} + + + +Paragraphs::ConstIterator Paragraphs::end() const +{ + return text.end(); +} diff --git a/source/Paragraphs.h b/source/Paragraphs.h new file mode 100644 index 000000000000..415cf64ef270 --- /dev/null +++ b/source/Paragraphs.h @@ -0,0 +1,66 @@ +/* Paragraphs.h +Copyright (c) 2024 by an anonymous author + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#ifndef PARAGRAPHS_H_ +#define PARAGRAPHS_H_ + +#include "ConditionSet.h" + +#include +#include +#include + +class ConditionsStore; +class DataNode; + + + +// Stores a list of description paragraphs, and a condition under which each should be shown. +// See the planet and spaceport description code for examples. +class Paragraphs { +public: + using ConditionalText = std::vector>; + using ConstIterator = ConditionalText::const_iterator; + + +public: + // Load one line of text and possible conditions from the given node. + void Load(const DataNode &node); + + // Discard all description lines. + void Clear(); + + // Is this object totally void of all information? + bool IsEmpty() const; + + // Are there any lines which match these vars? + bool IsEmptyFor(const ConditionsStore &vars) const; + + // Concatenate all lines which match these vars. + std::string ToString(const ConditionsStore &vars) const; + + // Iterate over all text. Needed to support PrintData. + // These must use standard naming conventions (begin, end) for compatibility with range-based for loops. + ConstIterator begin() const; + ConstIterator end() const; + + +private: + ConditionalText text; +}; + + + +#endif diff --git a/source/Planet.cpp b/source/Planet.cpp index b21c1a870d3c..7dfd6217f186 100644 --- a/source/Planet.cpp +++ b/source/Planet.cpp @@ -102,7 +102,7 @@ void Planet::Load(const DataNode &node, Set &wormholes) else if(key == "attributes") attributes.clear(); else if(key == "description") - description.clear(); + description.Clear(); else if(key == "port" || key == "spaceport") { port = Port(); @@ -178,17 +178,12 @@ void Planet::Load(const DataNode &node, Set &wormholes) landscape = SpriteSet::Get(value); else if(key == "music") music = value; - else if(key == "description" || key == "spaceport") + else if(key == "description") + description.Load(child); + else if(key == "spaceport") { - const bool isDescription = key == "description"; - if(!isDescription) - port.LoadDefaultSpaceport(); - - string &text = isDescription ? description : port.Description(); - if(!text.empty() && !value.empty() && value[0] > ' ') - text += '\t'; - text += value; - text += '\n'; + port.LoadDefaultSpaceport(); + port.LoadDescription(child); } else if(key == "government") government = GameData::Governments().Get(value); @@ -348,7 +343,7 @@ const string &Planet::TrueName() const // Get the planet's descriptive text. -const string &Planet::Description() const +const Paragraphs &Planet::Description() const { return description; } diff --git a/source/Planet.h b/source/Planet.h index 865606d1603e..2ae31037bea0 100644 --- a/source/Planet.h +++ b/source/Planet.h @@ -16,6 +16,7 @@ this program. If not, see . #ifndef PLANET_H_ #define PLANET_H_ +#include "Paragraphs.h" #include "Port.h" #include "Sale.h" @@ -68,8 +69,8 @@ class Planet { void SetName(const std::string &name); // Get the name used for this planet in the data files. const std::string &TrueName() const; - // Get the planet's descriptive text. - const std::string &Description() const; + // Return the description text for the planet, but not the spaceport: + const Paragraphs &Description() const; // Get the landscape sprite. const Sprite *Landscape() const; // Get the name of the ambient audio to play on this planet. @@ -159,7 +160,7 @@ class Planet { private: bool isDefined = false; std::string name; - std::string description; + Paragraphs description; Port port; const Sprite *landscape = nullptr; std::string music; diff --git a/source/PlanetPanel.cpp b/source/PlanetPanel.cpp index f3fdf10f815a..f2f76b5eb66e 100644 --- a/source/PlanetPanel.cpp +++ b/source/PlanetPanel.cpp @@ -149,7 +149,7 @@ void PlanetPanel::Draw() Rectangle box = ui.GetBox("content"); if(box.Width() != text.WrapWidth()) text.SetWrapWidth(box.Width()); - text.Wrap(planet.Description()); + text.Wrap(planet.Description().ToString(player.Conditions())); text.Draw(box.TopLeft(), *GameData::Colors().Get("bright")); } } diff --git a/source/Port.cpp b/source/Port.cpp index 71e276494697..bd595e64fa76 100644 --- a/source/Port.cpp +++ b/source/Port.cpp @@ -101,11 +101,7 @@ void Port::Load(const DataNode &node) hasNews = true; else if(key == "description" && child.Size() >= 2) { - const string &value = child.Token(1); - if(!description.empty() && !value.empty() && value[0] > ' ') - description += '\t'; - description += value; - description += '\n'; + description.Load(child); // If we have a description but no name then use the default spaceport name. if(name.empty()) @@ -138,6 +134,13 @@ void Port::LoadUninhabitedSpaceport() +void Port::LoadDescription(const DataNode &node) +{ + description.Load(node); +} + + + bool Port::CustomLoaded() const { return loaded; @@ -168,14 +171,7 @@ const string &Port::Name() const -string &Port::Description() -{ - return description; -} - - - -const string &Port::Description() const +const Paragraphs &Port::Description() const { return description; } diff --git a/source/Port.h b/source/Port.h index 973569607a3a..92d7b9e13aa0 100644 --- a/source/Port.h +++ b/source/Port.h @@ -20,6 +20,8 @@ this program. If not, see . #ifndef PORT_H_ #define PORT_H_ +#include "Paragraphs.h" + #include class DataNode; @@ -63,6 +65,9 @@ class Port void LoadDefaultSpaceport(); void LoadUninhabitedSpaceport(); + // Load a port's description text paragraphs from the planet spaceport description. + void LoadDescription(const DataNode &node); + // Whether this port was loaded from the Load function. bool CustomLoaded() const; @@ -73,8 +78,7 @@ class Port int GetRecharges() const; const std::string &Name() const; - std::string &Description(); - const std::string &Description() const; + const Paragraphs &Description() const; // Check whether the given recharging is possible. bool CanRecharge(int type) const; @@ -93,7 +97,7 @@ class Port // The description of this port. Shown when clicking on the // port button on the planet panel. - std::string description; + Paragraphs description; // What is recharged when landing on this port. int recharge = RechargeType::None; diff --git a/source/PrintData.cpp b/source/PrintData.cpp index 762f329763f7..bdfadf016034 100644 --- a/source/PrintData.cpp +++ b/source/PrintData.cpp @@ -595,8 +595,12 @@ namespace { { cout << it.first << "::"; const Planet &planet = it.second; - cout << planet.Description() << "::"; - cout << planet.GetPort().Description() << "\n"; + for(auto &whenText : planet.Description()) + cout << whenText.second; + cout << "::"; + for(auto &whenText : planet.GetPort().Description()) + cout << whenText.second; + cout << "\n"; } }; diff --git a/source/SpaceportPanel.cpp b/source/SpaceportPanel.cpp index 630412ac9b0d..174c8bbb2e74 100644 --- a/source/SpaceportPanel.cpp +++ b/source/SpaceportPanel.cpp @@ -92,7 +92,7 @@ void SpaceportPanel::Draw() return; Rectangle box = ui.GetBox("content"); - text.Wrap(port.Description()); + text.Wrap(port.Description().ToString(player.Conditions())); text.Draw(box.TopLeft(), *GameData::Colors().Get("bright")); if(hasNews) diff --git a/source/Wormhole.cpp b/source/Wormhole.cpp index c90eab92051f..fc692a8c223f 100644 --- a/source/Wormhole.cpp +++ b/source/Wormhole.cpp @@ -133,7 +133,7 @@ void Wormhole::Load(const DataNode &node) void Wormhole::LoadFromPlanet(const Planet &planet) { this->planet = &planet; - mappable = !planet.Description().empty(); + mappable = !planet.Description().IsEmpty(); GenerateLinks(); isAutogenerated = true; isDefined = true; From 80becd634a25db61dab2553d2c0f8ac7b96c28fb Mon Sep 17 00:00:00 2001 From: ziproot <109186806+ziproot@users.noreply.github.com> Date: Sun, 23 Jun 2024 04:56:56 -0400 Subject: [PATCH 38/75] feat(balance): Add energy generation and engine energy/heat to the Penguin (#9704) --- data/remnant/remnant ships.txt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/data/remnant/remnant ships.txt b/data/remnant/remnant ships.txt index f5c99cbd80c7..24ee67e42738 100644 --- a/data/remnant/remnant ships.txt +++ b/data/remnant/remnant ships.txt @@ -1083,7 +1083,7 @@ ship "Penguin" "bunks" 3 "mass" 89 "drag" 1.37 - "heat dissipation" 0.82 + "heat dissipation" 0.88 "fuel capacity" 800 "energy capacity" 300 "ramscoop" 1.25 @@ -1107,9 +1107,17 @@ ship "Penguin" "scramble protection" 0.25 "ion resistance" 0.05 "scramble resistance" 0.05 - "thrust" 4.2 - "turn" 80.0 - "reverse thrust" 4.2 + "thrust" 6.3 + "thrusting energy" 0.6 + "thrusting heat" 0.55 + "turn" 120.0 + "turning energy" 0.22 + "turning heat" 0.24 + "reverse thrust" 6.3 + "reverse thrusting energy" 0.55 + "reverse thrusting heat" 0.55 + "energy generation" 0.6 + "heat generation" 1 "flare sprite" "effect/remnant flare/small" "frame rate" 5 "flare sound" "plasma tiny" From a9fa3f14c527dc71d7d609cd2dcf0c53020c6432 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Sun, 23 Jun 2024 21:49:09 +0200 Subject: [PATCH 39/75] feat(mechanics): Mission actions no longer run after failure unless given a "can trigger after failure" child node (#10046) --- source/Mission.cpp | 26 ++++++++++++++------------ source/MissionAction.cpp | 6 +++++- source/MissionAction.h | 6 +++++- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/source/Mission.cpp b/source/Mission.cpp index 7d758f56e908..22da03eff530 100644 --- a/source/Mission.cpp +++ b/source/Mission.cpp @@ -815,20 +815,21 @@ bool Mission::CanOffer(const PlayerInfo &player, const shared_ptr &boardin if(repeat && playerConditions.Get(name + ": offered") >= repeat) return false; + bool isFailed = IsFailed(player); auto it = actions.find(OFFER); - if(it != actions.end() && !it->second.CanBeDone(player, boardingShip)) + if(it != actions.end() && !it->second.CanBeDone(player, isFailed, boardingShip)) return false; it = actions.find(ACCEPT); - if(it != actions.end() && !it->second.CanBeDone(player, boardingShip)) + if(it != actions.end() && !it->second.CanBeDone(player, isFailed, boardingShip)) return false; it = actions.find(DECLINE); - if(it != actions.end() && !it->second.CanBeDone(player, boardingShip)) + if(it != actions.end() && !it->second.CanBeDone(player, isFailed, boardingShip)) return false; it = actions.find(DEFER); - if(it != actions.end() && !it->second.CanBeDone(player, boardingShip)) + if(it != actions.end() && !it->second.CanBeDone(player, isFailed, boardingShip)) return false; return true; @@ -842,12 +843,13 @@ bool Mission::CanAccept(const PlayerInfo &player) const if(!toAccept.Test(playerConditions)) return false; + bool isFailed = IsFailed(player); auto it = actions.find(OFFER); - if(it != actions.end() && !it->second.CanBeDone(player)) + if(it != actions.end() && !it->second.CanBeDone(player, isFailed)) return false; it = actions.find(ACCEPT); - if(it != actions.end() && !it->second.CanBeDone(player)) + if(it != actions.end() && !it->second.CanBeDone(player, isFailed)) return false; return HasSpace(player); } @@ -896,7 +898,7 @@ bool Mission::IsSatisfied(const PlayerInfo &player) const // Determine if any fines or outfits that must be transferred, can. auto it = actions.find(COMPLETE); - if(it != actions.end() && !it->second.CanBeDone(player)) + if(it != actions.end() && !it->second.CanBeDone(player, IsFailed(player))) return false; // NPCs which must be accompanied or evaded must be present (or not), @@ -1092,7 +1094,7 @@ bool Mission::Do(Trigger trigger, PlayerInfo &player, UI *ui, const shared_ptrsecond.CanBeDone(player, boardingShip)) + if(it != actions.end() && !it->second.CanBeDone(player, IsFailed(player), boardingShip)) return false; if(trigger == ACCEPT) @@ -1193,7 +1195,7 @@ bool Mission::HasShip(const shared_ptr &ship) const // about it. This may affect the mission status or display a message. void Mission::Do(const ShipEvent &event, PlayerInfo &player, UI *ui) { - if(event.TargetGovernment()->IsPlayer() && !hasFailed) + if(event.TargetGovernment()->IsPlayer() && !IsFailed(player)) { bool failed = false; string message; @@ -1225,7 +1227,7 @@ void Mission::Do(const ShipEvent &event, PlayerInfo &player, UI *ui) } } - if((event.Type() & ShipEvent::DISABLE) && event.Target().get() == player.Flagship()) + if((event.Type() & ShipEvent::DISABLE) && event.Target() == player.FlagshipPtr()) Do(DISABLED, player, ui); // Jump events are only created for the player's flagship. @@ -1615,7 +1617,7 @@ bool Mission::Enter(const System *system, PlayerInfo &player, UI *ui) { const auto eit = onEnter.find(system); const auto originalSize = didEnter.size(); - if(eit != onEnter.end() && !didEnter.count(&eit->second) && eit->second.CanBeDone(player)) + if(eit != onEnter.end() && !didEnter.count(&eit->second) && eit->second.CanBeDone(player, IsFailed(player))) { eit->second.Do(player, ui, this); didEnter.insert(&eit->second); @@ -1624,7 +1626,7 @@ bool Mission::Enter(const System *system, PlayerInfo &player, UI *ui) // which may use a LocationFilter to govern which systems it can be performed in. else for(MissionAction &action : genericOnEnter) - if(!didEnter.count(&action) && action.CanBeDone(player)) + if(!didEnter.count(&action) && action.CanBeDone(player, IsFailed(player))) { action.Do(player, ui, this); didEnter.insert(&action); diff --git a/source/MissionAction.cpp b/source/MissionAction.cpp index ef03b490b123..1b7f9a572f05 100644 --- a/source/MissionAction.cpp +++ b/source/MissionAction.cpp @@ -181,6 +181,8 @@ void MissionAction::LoadSingle(const DataNode &child) else child.PrintTrace("Error: Unsupported use of \"system\" LocationFilter:"); } + else if(key == "can trigger after failure") + runsWhenFailed = true; else action.LoadSingle(child); } @@ -269,8 +271,10 @@ const string &MissionAction::DialogText() const // Check if this action can be completed right now. It cannot be completed // if it takes away money or outfits that the player does not have. -bool MissionAction::CanBeDone(const PlayerInfo &player, const shared_ptr &boardingShip) const +bool MissionAction::CanBeDone(const PlayerInfo &player, bool isFailed, const shared_ptr &boardingShip) const { + if(isFailed && !runsWhenFailed) + return false; if(player.Accounts().Credits() < -Payment()) return false; diff --git a/source/MissionAction.h b/source/MissionAction.h index f15e91a6bfdb..6ee8471096ea 100644 --- a/source/MissionAction.h +++ b/source/MissionAction.h @@ -62,7 +62,8 @@ class MissionAction { // Check if this action can be completed right now. It cannot be completed // if it takes away money or outfits that the player does not have, or should // take place in a system that does not match the specified LocationFilter. - bool CanBeDone(const PlayerInfo &player, const std::shared_ptr &boardingShip = nullptr) const; + // It can also not be done if the mission is failed, and teh trigger doesn't support it. + bool CanBeDone(const PlayerInfo &player, bool isFailed, const std::shared_ptr &boardingShip = nullptr) const; // Check if this action requires this ship to exist in order to ever be completed. bool RequiresGiftedShip(const std::string &shipId) const; // Perform this action. If a conversation is shown, the given destination @@ -98,6 +99,9 @@ class MissionAction { private: + // Whether this action can be triggered after the mission has failed. + bool runsWhenFailed = false; + std::string trigger; std::string system; LocationFilter systemFilter; From c8879006b6bdf2ff2eff8d0f1e8a99cad992db78 Mon Sep 17 00:00:00 2001 From: Peter van der Meer Date: Sun, 23 Jun 2024 22:06:35 +0200 Subject: [PATCH 40/75] fix(mechanics): Escorts stop following orders after the flagship dies (#10259) --- source/AI.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/AI.cpp b/source/AI.cpp index 29cc36afcb4f..b10461da0b1c 100644 --- a/source/AI.cpp +++ b/source/AI.cpp @@ -1550,9 +1550,12 @@ bool AI::FollowOrders(Ship &ship, Command &command) const int type = it->second.type; + // Ships without an (alive) parent don't follow orders. + shared_ptr parent = ship.GetParent(); + if(!parent) + return false; // If your parent is jumping or absent, that overrides your orders unless // your orders are to hold position. - shared_ptr parent = ship.GetParent(); if(parent && type != Orders::HOLD_POSITION && type != Orders::HOLD_ACTIVE && type != Orders::MOVE_TO) { if(parent->GetSystem() != ship.GetSystem()) From 93a3efd0c5559ce1d2e6a9c6ee39a95051df931f Mon Sep 17 00:00:00 2001 From: roadrunner56 <65418682+roadrunner56@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:56:08 -0600 Subject: [PATCH 41/75] fix(content): Restrict certain Quarg conversations to Human and Hai Quarg (#10226) --- data/quarg/quarg missions.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/data/quarg/quarg missions.txt b/data/quarg/quarg missions.txt index 1cbb2abc0af6..477b743ce630 100644 --- a/data/quarg/quarg missions.txt +++ b/data/quarg/quarg missions.txt @@ -234,9 +234,8 @@ mission "Ask Quarg About Coalition Early" not "joined the lunarium" not "joined the heliarchs" source - attributes "quarg" + attributes "hai quarg" "human quarg" attributes "station" - not attributes "gegno quarg" on offer conversation `You've met the Coalition, and the Heliarchs that rule their space from within the ringworlds the Quarg built there who claim that they "defeated the Quarg oppressors." Would you like to look for some Quarg here and ask for their side of the story?` @@ -267,9 +266,8 @@ mission "Quarg Pug Arfecta Warning" minor landing source - attributes "quarg" + attributes "hai quarg" "human quarg" attributes "ringworld" - not attributes "gegno quarg" to offer has "First Contact: Quarg: offered" has "ship model: Pug Arfecta" From 3fb1dbe428ffc23b702c414a5c415b8162c94bc6 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Thu, 27 Jun 2024 20:34:57 +0300 Subject: [PATCH 42/75] fix(typo): Argosy Hijacking Typos (#10252) --- data/human/human missions.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/human/human missions.txt b/data/human/human missions.txt index 6beffdd0d2c5..9dc6e3e71a2c 100644 --- a/data/human/human missions.txt +++ b/data/human/human missions.txt @@ -5971,7 +5971,7 @@ mission "Argosy Hijacking" ` The officer, slightly flustered, calls for a small squadron of Syndicated Security ships to chase the hijacked , then runs off to one of 's spaceport terminals to file the paperwork with the captain.` decline - npc save disable + npc disable save personality launching daring uninterested target confusion 80 government "Bounty (Disguised)" @@ -6014,7 +6014,7 @@ mission "Argosy Hijacking" ` He sighs and says, "Man, I don't have time for this. Tell me what actually happened."` choice ` "I blew it up, didn't you hear me?"` - ` "You ship got away. That's what happened."` + ` "Your ship got away. That's what happened."` ` "If you're going to waste my time, get out of here." Before you can react, he storms off towards the Syndicated Security offices.` decline @@ -6049,7 +6049,7 @@ mission "Argosy Hijacking" choice ` "Well, yeah."` ` "No, that's not what I meant."` - ` The captain scoffs, staring you down again with even more intensity than before, and lets you go with a rough push. "I don't need your explanation, I don't think you could give me one. I just can't understand how you can walk away feeling alright that you've set back one man's life so much."` + ` The captain scoffs, staring you down again with even more intensity than before, and lets you go with a rough push. "I don't need your explanation, and I don't think you could give me one. I just can't understand how you can walk away feeling alright that you've set back one man's life so much."` ` Suddenly, he turns around and walks off towards the direction of the bar, leaving you to contemplate your actions.` decline From 7c991d25ce96479b82bcf7ef4e80d38f9a45dc47 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Thu, 27 Jun 2024 20:42:54 +0200 Subject: [PATCH 43/75] chore(ci): Bump runner versions (#10218) --- .github/workflows/cd.yaml | 7 ++- .github/workflows/cd_release.yaml | 7 ++- .github/workflows/ci.yml | 5 +- .github/workflows/steam.yml | 74 ------------------------------ CMakePresets.json | 6 +-- overlays/macos-arm64-release.cmake | 2 +- overlays/macos64-release.cmake | 2 +- 7 files changed, 13 insertions(+), 90 deletions(-) delete mode 100644 .github/workflows/steam.yml diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 41a716180748..4186d21af3fe 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -17,7 +17,7 @@ jobs: cd_appimage_x64: name: Linux - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: OUTPUT: Endless_Sky-continuous-x86_64.AppImage GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -27,9 +27,8 @@ jobs: show-progress: false - name: Install dependencies run: | - sudo rm /etc/apt/sources.list.d/* && sudo dpkg --clear-avail # Speed up installation and get rid of unwanted lists sudo apt-get update - sudo apt-get install -y --no-install-recommends libxmu-dev libxi-dev libgl-dev libglu1-mesa-dev libgles2-mesa-dev libwayland-dev libxkbcommon-dev libegl1-mesa-dev + sudo apt-get install -y --no-install-recommends libxmu-dev libxi-dev libgl-dev libglu1-mesa-dev libgles2-mesa-dev libwayland-dev libxkbcommon-dev libegl1-mesa-dev fuse - name: Setup sccache uses: ./.github/sccache - name: Adjust version strings @@ -85,7 +84,7 @@ jobs: cd_macos_x64: name: MacOS - runs-on: macos-12 + runs-on: macos-13 env: OUTPUT: EndlessSky-macOS-continuous.zip GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cd_release.yaml b/.github/workflows/cd_release.yaml index 2525cd7f75ad..d11eca68fd6e 100644 --- a/.github/workflows/cd_release.yaml +++ b/.github/workflows/cd_release.yaml @@ -10,7 +10,7 @@ on: jobs: release_appimage_x64: name: AppImage x64 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: ARCH: x86_64 OUTPUT: Endless_Sky-${{ inputs.release_version }}-x86_64.AppImage @@ -21,9 +21,8 @@ jobs: show-progress: false - name: Install dependencies run: | - sudo rm /etc/apt/sources.list.d/* && sudo dpkg --clear-avail # Speed up installation and get rid of unwanted lists sudo apt-get update - sudo apt-get install -y --no-install-recommends libxmu-dev libxi-dev libgl-dev libglu1-mesa-dev libgles2-mesa-dev libwayland-dev libxkbcommon-dev libegl1-mesa-dev + sudo apt-get install -y --no-install-recommends libxmu-dev libxi-dev libgl-dev libglu1-mesa-dev libgles2-mesa-dev libwayland-dev libxkbcommon-dev libegl1-mesa-dev fuse - name: Setup sccache uses: ./.github/sccache - uses: lukka/get-cmake@latest @@ -83,7 +82,7 @@ jobs: release_macos_universal: name: MacOS Universal - runs-on: macos-12 + runs-on: macos-13 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} OUTPUT: Endless-Sky-${{ inputs.release_version }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 947c99d71274..7e0e9ca5801f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-${{ matrix.os }} strategy: matrix: - os: [20.04, 22.04] + os: [22.04, 24.04] opengl: [GL, GLES] env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -55,7 +55,6 @@ jobs: show-progress: false - name: Install development dependencies run: | - sudo rm /etc/apt/sources.list.d/* && sudo dpkg --clear-avail # Speed up installation and get rid of unwanted lists sudo apt-get update sudo apt-get install -y --no-install-recommends libxmu-dev libxi-dev libgl-dev libglu1-mesa-dev libgles2-mesa-dev libwayland-dev libxkbcommon-dev libegl1-mesa-dev libosmesa6 mesa-utils libglvnd-dev x11-utils - name: Disable VM sound card @@ -142,7 +141,7 @@ jobs: name: MacOS needs: changed if: ${{ needs.changed.outputs.game_code == 'true' || needs.changed.outputs.unit_tests == 'true' || needs.changed.outputs.cmake_files == 'true' || needs.changed.outputs.ci_config == 'true' }} - runs-on: macos-12 + runs-on: macos-13 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/.github/workflows/steam.yml b/.github/workflows/steam.yml deleted file mode 100644 index 203c0910c660..000000000000 --- a/.github/workflows/steam.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Steam - -on: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - build_linux: - name: Linux - runs-on: ubuntu-22.04 - strategy: - matrix: - arch: [x64, x86] - env: - snapshot: latest-container-runtime-depot - runtime: steam-container-runtime.tar.gz - platform: com.valvesoftware.SteamRuntime.Platform-amd64,i386-sniper-runtime.tar.gz - steps: - - uses: actions/checkout@v4 - with: - show-progress: false - - # Build the binary - - name: Save Steam runtime version - id: steamrt-version - run: | - STEAM_VERSION=$(curl -sSf https://repo.steampowered.com/steamrt-images-sniper/snapshots/latest-container-runtime-depot/VERSION.txt --output -) - echo "RT_VERSION=$STEAM_VERSION" >> $GITHUB_ENV - - name: Build Endless Sky - run: | - cd steam - docker-compose run steam-${{ matrix.arch }} - - # Test the binary - - name: Install runtime dependencies - run: | - sudo rm /etc/apt/sources.list.d/* && sudo dpkg --clear-avail # Speed up installation and get rid of unwanted lists - sudo apt-get update - sudo apt-get install -y --no-install-recommends libosmesa6 mesa-utils - - name: Restore cached Steam Runtime environment - id: cache-runtime - uses: actions/cache@v4 - with: - path: | - ${{ env.runtime }} - ${{ env.platform }} - key: steamrt-${{ env.RT_VERSION }} - - name: Download Steam Runtime environment - if: steps.cache-runtime.outputs.cache-hit != 'true' - run: | - curl -sSf https://repo.steampowered.com/steamrt-images-sniper/snapshots/${{ env.snapshot }}/${{ env.runtime }} > ${{ env.runtime }} - curl -sSf https://repo.steampowered.com/steamrt-images-sniper/snapshots/${{ env.snapshot }}/${{ env.platform }} > ${{ env.platform }} - - name: Extract Steam Runtime - run: | - tar -xf ${{ env.runtime }} - tar -xf ${{ env.platform }} -C steam-container-runtime/depot/sniper_platform_*/ - - name: Verify executable - run: ./steam-container-runtime/depot/run-in-sniper ./build/steam-${{ matrix.arch }}/endless-sky -- -v - - name: Execute data parsing test - run: ./steam-container-runtime/depot/run-in-sniper ./build/steam-${{ matrix.arch }}/endless-sky -- -p - - name: Execute integration data parsing test - run: ./steam-container-runtime/depot/run-in-sniper ./build/steam-${{ matrix.arch }}/endless-sky -- -p --config tests/integration/config - - name: Execute tests - run: | - cd steam - docker-compose run test-steam-${{ matrix.arch }} - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: binary-steam-${{ matrix.arch }} - path: build/steam-${{ matrix.arch }}/endless-sky diff --git a/CMakePresets.json b/CMakePresets.json index de3de538fb1e..fb0fe7dee5ac 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -214,7 +214,7 @@ "VCPKG_HOST_TRIPLET": "macos64-release", "VCPKG_TARGET_TRIPLET": "macos64-release", "CMAKE_OSX_ARCHITECTURES": "x86_64", - "CMAKE_OSX_DEPLOYMENT_TARGET": "10.9" + "CMAKE_OSX_DEPLOYMENT_TARGET": "13" } }, { @@ -262,7 +262,7 @@ "VCPKG_HOST_TRIPLET": "macos64-release", "VCPKG_TARGET_TRIPLET": "macos64-release", "CMAKE_OSX_ARCHITECTURES": "x86_64", - "CMAKE_OSX_DEPLOYMENT_TARGET": "10.9" + "CMAKE_OSX_DEPLOYMENT_TARGET": "13" } }, { @@ -280,7 +280,7 @@ "VCPKG_HOST_TRIPLET": "macos64-release", "VCPKG_TARGET_TRIPLET": "macos-arm64-release", "CMAKE_OSX_ARCHITECTURES": "arm64", - "CMAKE_OSX_DEPLOYMENT_TARGET": "10.9" + "CMAKE_OSX_DEPLOYMENT_TARGET": "13" } }, { diff --git a/overlays/macos-arm64-release.cmake b/overlays/macos-arm64-release.cmake index e3f9152ea086..d1776e23a2c7 100644 --- a/overlays/macos-arm64-release.cmake +++ b/overlays/macos-arm64-release.cmake @@ -5,4 +5,4 @@ set(VCPKG_BUILD_TYPE release) set(VCPKG_CMAKE_SYSTEM_NAME Darwin) set(VCPKG_OSX_ARCHITECTURES arm64) -set(VCPKG_OSX_DEPLOYMENT_TARGET 10.9) +set(VCPKG_OSX_DEPLOYMENT_TARGET 13) diff --git a/overlays/macos64-release.cmake b/overlays/macos64-release.cmake index 4fae2dd38f7d..42b38847f76b 100644 --- a/overlays/macos64-release.cmake +++ b/overlays/macos64-release.cmake @@ -5,4 +5,4 @@ set(VCPKG_BUILD_TYPE release) set(VCPKG_CMAKE_SYSTEM_NAME Darwin) set(VCPKG_OSX_ARCHITECTURES x86_64) -set(VCPKG_OSX_DEPLOYMENT_TARGET 10.9) +set(VCPKG_OSX_DEPLOYMENT_TARGET 13) From bbfcf2863464db42e8b6a674235088cebf99f782 Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:28:07 +0200 Subject: [PATCH 44/75] fix(content): Fix `Hold position` tooltip text (#10278) --- data/_ui/tooltips.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/_ui/tooltips.txt b/data/_ui/tooltips.txt index 3dca41176a59..22788b5db1d0 100644 --- a/data/_ui/tooltips.txt +++ b/data/_ui/tooltips.txt @@ -1303,7 +1303,7 @@ tip "Fleet: Gather around me" `Command your fleet to gather around your flagship. If only some escorts are selected, only sends the command to those escorts. You can also command escorts to gather around another ship by right clicking on a friendly ship.` tip "Fleet: Hold position" - `Command your fleet to gather around your flagship. If only some escorts are selected, only sends the command to those escorts. This command can also be sent by right clicking on empty space.` + `Command your fleet to hold its current position. If only some escorts are selected, only sends the command to those escorts. This command can also be sent by right clicking on empty space.` tip "Fleet: Toggle ammo usage" `Toggle the "Escorts expend ammo" setting.` From c56fc67a0bb8e90ad64bcdf805ea7e4a62a249fb Mon Sep 17 00:00:00 2001 From: Peter van der Meer Date: Fri, 28 Jun 2024 21:37:35 +0200 Subject: [PATCH 45/75] feat(enhancement): Formation flying with simple patterns (#10039) --- data/_ui/tooltips.txt | 1 + data/formations_basic.txt | 78 +++++++ source/AI.cpp | 177 +++++++++++++++- source/AI.h | 13 +- source/CMakeLists.txt | 2 + source/Fleet.cpp | 4 + source/Fleet.h | 2 + source/FormationPattern.cpp | 251 ++++++++++++++++++++-- source/FormationPattern.h | 97 ++++++++- source/FormationPositioner.cpp | 252 +++++++++++++++++++++++ source/FormationPositioner.h | 95 +++++++++ source/Ship.cpp | 20 +- source/Ship.h | 6 + source/UniverseObjects.cpp | 1 - tests/unit/src/test_formationPattern.cpp | 29 ++- 15 files changed, 986 insertions(+), 42 deletions(-) create mode 100644 data/formations_basic.txt create mode 100644 source/FormationPositioner.cpp create mode 100644 source/FormationPositioner.h diff --git a/data/_ui/tooltips.txt b/data/_ui/tooltips.txt index 22788b5db1d0..c48a7f3584f8 100644 --- a/data/_ui/tooltips.txt +++ b/data/_ui/tooltips.txt @@ -1301,6 +1301,7 @@ tip "Fleet: Fight my target" tip "Fleet: Gather around me" `Command your fleet to gather around your flagship. If only some escorts are selected, only sends the command to those escorts. You can also command escorts to gather around another ship by right clicking on a friendly ship.` + `With , change the formation pattern in your fleet.` tip "Fleet: Hold position" `Command your fleet to hold its current position. If only some escorts are selected, only sends the command to those escorts. This command can also be sent by right clicking on empty space.` diff --git a/data/formations_basic.txt b/data/formations_basic.txt new file mode 100644 index 000000000000..97b6efb3e1ed --- /dev/null +++ b/data/formations_basic.txt @@ -0,0 +1,78 @@ +# Copyright (c) 2019-2021 by Peter van der Meer +# +# Endless Sky is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later version. +# +# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +# Formation where ships fly in a diagonal line left behind the formation leader. +formation "Echelon (left)" + position -150 150 + repeat -150 150 + + +# Formation where ships fly in a diagonal line right behind the formation leader. +formation "Echelon (right)" + position 150 150 + repeat 150 150 + + +# Formation where ships form a line right and left of the formation leader. +formation "Line (sides)" + flippable x y + rotatable 180 + position -200 0 + repeat -200 0 + position 200 0 + repeat 200 0 + + +# Formation where ships form a line behind the formation leader. +formation "Line (trailing)" + flippable y + position 0 200 + repeat 0 200 + + +# Formation with two legs extending on a 30 degree angle behind the lead ship. +formation "Vic" + flippable y + position -150 87 + repeat -150 87 + position 150 87 + repeat 150 87 + + +# X shaped formation that rotates with the flagship. +# For warship-sized escorts +formation "X" + flippable x y + rotatable 90 + position -150 150 + repeat -150 150 + position 150 150 + repeat 150 150 + position 150 -150 + repeat 150 -150 + position -150 -150 + repeat -150 -150 + + +# Stable X or + shaped formation that doesn't rotate with the flagship. +formation "X stable" + rotatable 0 + position -150 150 + repeat -150 150 + position 150 150 + repeat 150 150 + position 150 -150 + repeat 150 -150 + position -150 -150 + repeat -150 -150 diff --git a/source/AI.cpp b/source/AI.cpp index b10461da0b1c..54d8373a11e8 100644 --- a/source/AI.cpp +++ b/source/AI.cpp @@ -20,6 +20,8 @@ this program. If not, see . #include "DistanceMap.h" #include "FighterHitHelper.h" #include "Flotsam.h" +#include "FormationPattern.h" +#include "FormationPositioner.h" #include "GameData.h" #include "Gamerules.h" #include "Government.h" @@ -161,6 +163,36 @@ namespace { bay.ship->SetCommands(Command::DEPLOY); } + // Helper function for selecting the ships for formation commands. + vector GetShipsForFormationCommand(const PlayerInfo &player) + { + // Figure out what ships we are giving orders to. + vector targetShips; + auto &selectedShips = player.SelectedShips(); + // If selectedShips is empty, this applies to the whole fleet. + if(selectedShips.empty()) + { + auto &playerShips = player.Ships(); + targetShips.reserve(playerShips.size() - 1); + for(const shared_ptr &it : player.Ships()) + if(it.get() != player.Flagship() && !it->IsParked() + && !it->IsDestroyed() && !it->IsDisabled()) + targetShips.push_back(it.get()); + } + else + { + targetShips.reserve(selectedShips.size()); + for(const weak_ptr &it : selectedShips) + { + shared_ptr ship = it.lock(); + if(ship) + targetShips.push_back(ship.get()); + } + } + + return targetShips; + } + // Issue deploy orders for the selected ships (or the full fleet if no ships are selected). void IssueDeploy(const PlayerInfo &player) { @@ -331,6 +363,72 @@ AI::AI(const PlayerInfo &player, const List &ships, // Fleet commands from the player. +void AI::IssueFormationChange(PlayerInfo &player) +{ + // Figure out what ships we are giving orders to + vector targetShips = GetShipsForFormationCommand(player); + if(targetShips.empty()) + return; + + const auto &formationPatterns = GameData::Formations(); + if(formationPatterns.empty()) + { + Messages::Add("No formations available.", Messages::Importance::High); + return; + } + + // First check which and how many formation patterns we have in the current set of selected ships. + // If there is more than 1 pattern in the selection, then this command will change all selected + // ships to use the pattern that was found first. If there is only 1 pattern, then this command + // will change all selected ships to use the next pattern available. + const FormationPattern *toSet = nullptr; + bool multiplePatternsSet = false; + for(Ship *ship : targetShips) + { + const FormationPattern *shipsPattern = ship->GetFormationPattern(); + if(shipsPattern) + { + if(!toSet) + toSet = shipsPattern; + else if(toSet != shipsPattern) + { + multiplePatternsSet = true; + break; + } + } + } + + // Now determine what formation pattern to set. + if(!toSet) + // If no pattern was set at all, then we set the first one from the set of formationPatterns. + toSet = &(formationPatterns.begin()->second); + else if(!multiplePatternsSet) + { + // If only one pattern was found, then select the next pattern (or clear the pattern if there is no next). + auto it = formationPatterns.find(toSet->Name()); + if(it != formationPatterns.end()) + ++it; + toSet = (it == formationPatterns.end() ? nullptr : &(it->second)); + } + // If more than one formation was found on the ships, then we set the pattern it to the first one found. + // No code is needed here for this option, since all variables are already set to just apply the change below. + + // Now set the pattern on the selected ships, and tell them to gather. + for(Ship *ship : targetShips) + { + ship->SetFormationPattern(toSet); + orders[ship].type = Orders::GATHER; + orders[ship].target = player.FlagshipPtr(); + } + + unsigned int count = targetShips.size(); + string message = to_string(count) + (count == 1 ? " ship" : " ships") + " will "; + message += toSet ? ("assume \"" + toSet->Name() + "\" formation.") : "no longer fly in formation."; + Messages::Add(message, Messages::Importance::Low); +} + + + void AI::IssueShipTarget(const shared_ptr &target) { Orders newOrders; @@ -416,23 +514,28 @@ void AI::UpdateKeys(PlayerInfo &player, Command &activeCommands) if(activeCommands.Has(Command::DEPLOY)) IssueDeploy(player); + // The gather command controls formation flying when combined with shift. + const bool shift = activeCommands.Has(Command::SHIFT); + if(shift && activeCommands.Has(Command::GATHER)) + IssueFormationChange(player); + shared_ptr target = flagship->GetTargetShip(); shared_ptr targetAsteroid = flagship->GetTargetAsteroid(); Orders newOrders; - if(activeCommands.Has(Command::FIGHT) && target && !target->IsYours()) + if(activeCommands.Has(Command::FIGHT) && target && !target->IsYours() && !shift) { newOrders.type = target->IsDisabled() ? Orders::FINISH_OFF : Orders::ATTACK; newOrders.target = target; IssueOrders(newOrders, "focusing fire on \"" + target->Name() + "\"."); } - else if(activeCommands.Has(Command::FIGHT) && targetAsteroid) + else if(activeCommands.Has(Command::FIGHT) && !shift && targetAsteroid) IssueAsteroidTarget(targetAsteroid); - if(activeCommands.Has(Command::HOLD)) + if(activeCommands.Has(Command::HOLD) && !shift) { newOrders.type = Orders::HOLD_POSITION; IssueOrders(newOrders, "holding position."); } - if(activeCommands.Has(Command::GATHER)) + if(activeCommands.Has(Command::GATHER) && !shift) { newOrders.type = Orders::GATHER; newOrders.target = player.FlagshipPtr(); @@ -511,6 +614,7 @@ void AI::UpdateEvents(const list &events) // the player has entered a new one. void AI::Clean() { + // Records of what various AI ships and factions have done. actions.clear(); notoriety.clear(); governmentActions.clear(); @@ -525,6 +629,9 @@ void AI::Clean() miningRadius.clear(); miningTime.clear(); appeasementThreshold.clear(); + // Records for formations flying around lead ships and other objects. + formations.clear(); + // Records that affect the combat behavior of various governments. shipStrength.clear(); enemyStrength.clear(); allyStrength.clear(); @@ -571,6 +678,12 @@ void AI::Step(Command &activeCommands) } } + // Allow all formation-positioners to handle their internal administration to + // prepare for the next cycle. + for(auto &bodyIt : formations) + for(auto &positionerIt : bodyIt.second) + positionerIt.second.Step(); + const Ship *flagship = player.Flagship(); step = (step + 1) & 31; int targetTurn = 0; @@ -1542,7 +1655,8 @@ vector AI::GetShipsList(const Ship &ship, bool targetEnemies, double max -bool AI::FollowOrders(Ship &ship, Command &command) const +// TODO: This should be const when ships are not added and removed from formations in MoveInFormation +bool AI::FollowOrders(Ship &ship, Command &command) { auto it = orders.find(&ship); if(it == orders.end()) @@ -1619,7 +1733,12 @@ bool AI::FollowOrders(Ship &ship, Command &command) const else if(type == Orders::KEEP_STATION) KeepStation(ship, command, *target); else if(type == Orders::GATHER) - CircleAround(ship, command, *target); + { + if(ship.GetFormationPattern()) + MoveInFormation(ship, command); + else + CircleAround(ship, command, *target); + } else MoveIndependent(ship, command); @@ -1628,6 +1747,44 @@ bool AI::FollowOrders(Ship &ship, Command &command) const +void AI::MoveInFormation(Ship &ship, Command &command) +{ + shared_ptr parent = ship.GetParent(); + if(!parent) + return; + + const Body *formationLead = parent.get(); + const FormationPattern *pattern = ship.GetFormationPattern(); + + // First we retrieve the patterns that are formed around the parent. + auto &patterns = formations[formationLead]; + + // Find the existing FormationPositioner for the pattern, or add one if none exists yet. + auto insert = patterns.emplace(piecewise_construct, + forward_as_tuple(pattern), + forward_as_tuple(formationLead, pattern)); + + // Set an iterator to point to the just found or emplaced value. + auto it = insert.first; + + // Aggressively try to match the position and velocity for the formation position. + const double POSITION_DEADBAND = ship.Radius() * 1.25; + constexpr double VELOCITY_DEADBAND = .1; + bool inPosition = MoveTo(ship, command, it->second.Position(&ship), formationLead->Velocity(), POSITION_DEADBAND, + VELOCITY_DEADBAND); + + // If we match the position and velocity, then also match the facing angle within some limits. + constexpr double FACING_TOLERANCE_DEGREES = 3; + if(inPosition) + { + double facingDeltaDegrees = (formationLead->Facing() - ship.Facing()).Degrees(); + if(abs(facingDeltaDegrees) > FACING_TOLERANCE_DEGREES) + command.SetTurn(facingDeltaDegrees); + } +} + + + void AI::MoveIndependent(Ship &ship, Command &command) const { double invisibleFenceRadius = ship.GetSystem()->InvisibleFenceRadius(); @@ -1845,7 +2002,8 @@ void AI::MoveIndependent(Ship &ship, Command &command) const -void AI::MoveEscort(Ship &ship, Command &command) const +// TODO: Function should be const, but formation flying needed write access to the FormationPositioner. +void AI::MoveEscort(Ship &ship, Command &command) { const Ship &parent = *ship.GetParent(); const System *currentSystem = ship.GetSystem(); @@ -1983,11 +2141,15 @@ void AI::MoveEscort(Ship &ship, Command &command) const // This ship has no route to the parent's destination system, so protect it until it jumps away. KeepStation(ship, command, parent); } + else if(ship.GetFormationPattern()) + MoveInFormation(ship, command); else KeepStation(ship, command, parent); } else if(parent.Commands().Has(Command::BOARD) && parent.GetTargetShip().get() == &ship) Stop(ship, command, .2); + else if(ship.GetFormationPattern()) + MoveInFormation(ship, command); else KeepStation(ship, command, parent); } @@ -4667,7 +4829,6 @@ void AI::IssueOrders(const Orders &newOrders, const string &description) } who = ships.size() > 1 ? "The selected escorts are " : "The selected escort is "; } - // This should never happen, but just in case: if(ships.empty()) return; diff --git a/source/AI.h b/source/AI.h index 747f6d377f36..da8474a1f46e 100644 --- a/source/AI.h +++ b/source/AI.h @@ -18,6 +18,7 @@ this program. If not, see . #include "Command.h" #include "FireCommand.h" +#include "FormationPositioner.h" #include "Point.h" #include @@ -57,9 +58,11 @@ template const List &minables, const List &flotsam); // Fleet commands from the player. + void IssueFormationChange(PlayerInfo &player); void IssueShipTarget(const std::shared_ptr &target); void IssueAsteroidTarget(const std::shared_ptr &targetAsteroid); void IssueMoveTarget(const Point &target, const System *moveToSystem); + // Commands issued via the keyboard (mostly, to the flagship). void UpdateKeys(PlayerInfo &player, Command &clickCommands); @@ -97,9 +100,10 @@ template // Obtain a list of ships matching the desired hostility. std::vector GetShipsList(const Ship &ship, bool targetEnemies, double maxRange = -1.) const; - bool FollowOrders(Ship &ship, Command &command) const; + bool FollowOrders(Ship &ship, Command &command); + void MoveInFormation(Ship &ship, Command &command); void MoveIndependent(Ship &ship, Command &command) const; - void MoveEscort(Ship &ship, Command &command) const; + void MoveEscort(Ship &ship, Command &command); static void Refuel(Ship &ship, Command &command); static bool CanRefuel(const Ship &ship, const StellarObject *target); // Set the ship's target system or planet in order to reach the @@ -263,8 +267,11 @@ template std::map miningTime; std::map appeasementThreshold; - std::map shipStrength; + // Records for formations flying around leadships and other objects. + std::map> formations; + // Records that affect the combat behavior of various governments. + std::map shipStrength; std::map enemyStrength; std::map allyStrength; std::map> governmentRosters; diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 336f63aa2ed7..a27ab293e0d9 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -123,6 +123,8 @@ target_sources(EndlessSkyLib PRIVATE FogShader.h FormationPattern.cpp FormationPattern.h + FormationPositioner.cpp + FormationPositioner.h FrameTimer.cpp FrameTimer.h Galaxy.cpp diff --git a/source/Fleet.cpp b/source/Fleet.cpp index 9b37566c08f8..86f1d57fa636 100644 --- a/source/Fleet.cpp +++ b/source/Fleet.cpp @@ -16,6 +16,7 @@ this program. If not, see . #include "Fleet.h" #include "DataNode.h" +#include "FormationPattern.h" #include "GameData.h" #include "Government.h" #include "Logger.h" @@ -109,6 +110,8 @@ void Fleet::Load(const DataNode &node) cargo.LoadSingle(child); else if(key == "personality") personality.Load(child); + else if(key == "formation" && hasValue) + formation = GameData::Formations().Get(child.Token(1)); else if(key == "variant" && !remove) { if(resetVariants && !add) @@ -525,6 +528,7 @@ vector> Fleet::Instantiate(const vector &ships) c ship->SetPersonality(fighterPersonality); else ship->SetPersonality(personality); + ship->SetFormationPattern(formation); placed.push_back(ship); } diff --git a/source/Fleet.h b/source/Fleet.h index d54662968b66..c66c9155fdcb 100644 --- a/source/Fleet.h +++ b/source/Fleet.h @@ -30,6 +30,7 @@ this program. If not, see . #include class DataNode; +class FormationPattern; class Government; class Outfit; class Phrase; @@ -87,6 +88,7 @@ class Fleet { const Government *government = nullptr; const Phrase *names = nullptr; const Phrase *fighterNames = nullptr; + const FormationPattern *formation = nullptr; WeightedList variants; // The cargo ships in this fleet will carry. FleetCargo cargo; diff --git a/source/FormationPattern.cpp b/source/FormationPattern.cpp index ec0e6202a527..7eaec0c3f468 100644 --- a/source/FormationPattern.cpp +++ b/source/FormationPattern.cpp @@ -15,44 +15,124 @@ this program. If not, see . #include "FormationPattern.h" +#include "Angle.h" #include "DataNode.h" #include using namespace std; -namespace { - const Point defaultFormationPoint = Point(); -} - - -FormationPattern::PositionIterator::PositionIterator(const FormationPattern &pattern) - : pattern(pattern) +FormationPattern::PositionIterator::PositionIterator(const FormationPattern &pattern, + double centerBodyRadius) + : pattern(pattern), centerBodyRadius(centerBodyRadius) { - positionIt = pattern.positions.begin(); + MoveToValidPositionOutsideCenterBody(); } const Point &FormationPattern::PositionIterator::operator*() { - if(positionIt == pattern.positions.end()) - return defaultFormationPoint; - return *positionIt; + return currentPoint; } FormationPattern::PositionIterator &FormationPattern::PositionIterator::operator++() { - if(positionIt != pattern.positions.end()) - ++positionIt; + if(!atEnd) + { + position++; + MoveToValidPositionOutsideCenterBody(); + } + return *this; } +void FormationPattern::PositionIterator::MoveToValidPositionOutsideCenterBody() +{ + MoveToValidPosition(); + unsigned int maxTries = 50; + // Skip positions too close to the center body + while(!atEnd && currentPoint.Length() <= centerBodyRadius && maxTries > 0) + { + position++; + MoveToValidPosition(); + maxTries--; + } + if(maxTries == 0) + atEnd = true; +} + + + +void FormationPattern::PositionIterator::MoveToValidPosition() +{ + unsigned int lines = pattern.Lines(); + + // If we cannot calculate any new positions, then just return center point. + if(atEnd || lines < 1) + { + atEnd = true; + currentPoint = Point(); + return; + } + + unsigned int ringsScanned = 0; + unsigned int startingRing = ring; + unsigned int lineRepeatPositions = pattern.Positions(ring, line, repeat); + + while(position >= lineRepeatPositions && !atEnd) + { + unsigned int patternRepeats = pattern.Repeats(line); + // LinePosition number is beyond the amount of positions available on the line/arc. + // Need to move a ring, a line/arc or a repeat-section forward. + if(ring > 0 && line < lines && patternRepeats > 0 && repeat < patternRepeats - 1) + { + // First check if we are on a valid line and have another repeat section. + ++repeat; + position = 0; + lineRepeatPositions = pattern.Positions(ring, line, repeat); + } + else if(line < lines - 1) + { + // If we don't have another repeat section, then check for a next line. + ++line; + repeat = 0; + position = 0; + lineRepeatPositions = pattern.Positions(ring, line, repeat); + } + else + { + // If we checked all lines and repeat sections, then go for the next ring. + ++ring; + line = 0; + repeat = 0; + position = 0; + lineRepeatPositions = pattern.Positions(ring, line, repeat); + + // If we scanned more than 5 rings without finding a new position, then we have an empty pattern. + ++ringsScanned; + if(ringsScanned > 5) + { + // Restore starting ring and indicate that there are no more positions. + ring = startingRing; + atEnd = true; + } + } + } + + if(atEnd) + currentPoint = Point(); + else + currentPoint = pattern.Position(ring, line, repeat, position); +} + + + void FormationPattern::Load(const DataNode &node) { if(!name.empty()) @@ -70,8 +150,36 @@ void FormationPattern::Load(const DataNode &node) } for(const DataNode &child : node) - if(child.Token(0) == "position" && child.Size() >= 3) - positions.emplace_back(child.Value(1), child.Value(2)); + if(child.Token(0) == "flippable" && child.Size() >= 2) + for(int i = 1; i < child.Size(); ++i) + { + if(child.Token(i) == "x") + flippableX = true; + else if(child.Token(i) == "y") + flippableY = true; + else + child.PrintTrace("Skipping unrecognized attribute:"); + } + else if(child.Token(0) == "rotatable" && child.Size() >= 2) + rotatable = child.Value(1); + else if(child.Token(0) == "position" && child.Size() >= 3) + { + Line &line = lines.emplace_back(); + // A point is a line with just 1 position on it. + line.positions = 1; + // The specification of the coordinates is on the same line as the keyword. + line.start.Set(child.Value(1), child.Value(2)); + line.endOrAnchor = line.start; + // Also allow positions to have a repeat section, for single points only + for(const DataNode &grand : child) + if(grand.Token(0) == "repeat" && grand.Size() >= 3) + { + LineRepeat &repeat = line.repeats.emplace_back(); + repeat.repeatStart.Set(grand.Value(1), grand.Value(2)); + } + else + grand.PrintTrace("Skipping unrecognized attribute:"); + } else child.PrintTrace("Skipping unrecognized attribute:"); } @@ -93,7 +201,116 @@ void FormationPattern::SetName(const std::string &name) // Get an iterator to iterate over the formation positions in this pattern. -FormationPattern::PositionIterator FormationPattern::begin() const +FormationPattern::PositionIterator FormationPattern::begin(double centerBodyRadius) const +{ + return FormationPattern::PositionIterator(*this, centerBodyRadius); +} + + + +// Get the number of lines (and arcs) in this formation. +unsigned int FormationPattern::Lines() const +{ + return lines.size(); +} + + + +// Get the number of repeat sections for the given arc or line. +unsigned int FormationPattern::Repeats(unsigned int lineNr) const +{ + if(lineNr >= lines.size()) + return 0; + return lines[lineNr].repeats.size(); +} + + + +// Get the number of positions on an arc or line. +unsigned int FormationPattern::Positions(unsigned int ring, unsigned int lineNr, unsigned int repeatNr) const +{ + // Retrieve the relevant line. + if(lineNr >= lines.size()) + return 0; + const Line &line = lines[lineNr]; + + int lineRepeatPositions = line.positions; + + // For the very first ring, only the initial positions are relevant. + if(ring > 0) + { + // For later rings we need to have repeat sections to perform repeating. + if(repeatNr >= line.repeats.size()) + return 0; + + lineRepeatPositions += line.repeats[repeatNr].repeatPositions * ring; + } + + // If we are in a later ring, then skip lines that don't repeat. + if(lineRepeatPositions < 0) + return 0; + + return lineRepeatPositions; +} + + + +// Get a formation position based on ring, line-number and position on the line. +Point FormationPattern::Position(unsigned int ring, unsigned int lineNr, unsigned int repeatNr, + unsigned int linePosition) const +{ + // First check if the inputs result in a valid line position. + if(lineNr >= lines.size()) + return Point(); + const Line &line = lines[lineNr]; + if(ring > 0 && repeatNr >= line.repeats.size()) + return Point(); + + // Perform common start and end/anchor position calculations in pixels. + Point startPx = line.start; + Point endOrAnchorPx = line.endOrAnchor; + + // Get the number of positions for this line. + int positions = line.positions; + + // Check if we have a valid repeat section and apply it to the common calculations if we have it. + const LineRepeat *repeat = nullptr; + if(ring > 0) + { + repeat = &(line.repeats[repeatNr]); + startPx += repeat->repeatStart * ring; + endOrAnchorPx += repeat->repeatEndOrAnchor * ring; + positions += repeat->repeatPositions * ring; + } + + // Calculate the step from each position between start and end. + Point positionPx = endOrAnchorPx - startPx; + + // Divide by positions, but don't count the first (since it is at position 0, not at position 1). + if(positions > 1) + positionPx /= positions - 1; + + // Calculate position in the formation based on the position in the line. + return startPx + positionPx * linePosition; +} + + + +int FormationPattern::Rotatable() const +{ + return rotatable; +} + + + +bool FormationPattern::FlippableY() const +{ + return flippableY; +} + + + +bool FormationPattern::FlippableX() const { - return FormationPattern::PositionIterator(*this); + return flippableX; } diff --git a/source/FormationPattern.h b/source/FormationPattern.h index 1927c06bdbcd..e2a120482a57 100644 --- a/source/FormationPattern.h +++ b/source/FormationPattern.h @@ -21,6 +21,7 @@ this program. If not, see . #include #include +class Body; class DataNode; @@ -34,7 +35,9 @@ class FormationPattern { // Iterator that provides sequential access to all formation positions. class PositionIterator { public: - explicit PositionIterator(const FormationPattern &pattern); + explicit PositionIterator(const FormationPattern &pattern, + double centerBodyRadius); + PositionIterator() = delete; // Iterator traits @@ -49,16 +52,33 @@ class FormationPattern { const Point &operator*(); PositionIterator &operator++(); + private: + void MoveToValidPositionOutsideCenterBody(); + void MoveToValidPosition(); private: // The pattern for which we are calculating positions. const FormationPattern &pattern; - // The iterator currently used below the position iterator. - std::vector::const_iterator positionIt; - }; + // The iteration of the (repeating) pattern we are processing. + // Most formationpatterns grow from the inside to the outside. + unsigned int ring = 0; + // The line (or point, or arc) in the pattern that we are processing. + unsigned int line = 0; + // The active repeat-section on the line or arc. (Lines or arcs can have more than 1 repeat section) + unsigned int repeat = 0; + // The position on the current repeat section of the line or arc. + unsigned int position = 0; + // Center radius that is to be kept clear. This is used to avoid + // positions of ships overlapping with the body around which the + // formation is formed. + double centerBodyRadius = 0; + // Currently calculated Point. + Point currentPoint; + // Internal status variable; + bool atEnd = false; + }; -public: // Load formation from a datafile. void Load(const DataNode &node); @@ -67,14 +87,75 @@ class FormationPattern { void SetName(const std::string &name); // Get an iterator to iterate over the formation positions in this pattern. - PositionIterator begin() const; + PositionIterator begin(double centerBodyRadius) const; + + // Information about allowed rotating and mirroring that still results in the same formation. + int Rotatable() const; + bool FlippableY() const; + bool FlippableX() const; + +private: + // Retrieve properties like number of lines and arcs, number of repeat sections and number of positions. + // Private, because we hide those properties and just provide a position iterator instead. + unsigned int Lines() const; + // Number of repeat sections on the current line. + unsigned int Repeats(unsigned int lineNr) const; + // Number of positions on the current repeat section of the active line or arc. + unsigned int Positions(unsigned int ring, unsigned int lineNr, unsigned int repeatNr) const; + + // Calculate a position based on the current ring, line/arc, repeat-section and position on the line-repeat-section. + Point Position(unsigned int ring, unsigned int lineNr, unsigned int repeatNr, + unsigned int lineRepeatPosition) const; + +private: + class LineRepeat { + public: + // Vector to apply to get to the next start point for the next iteration. + Point repeatStart; + Point repeatEndOrAnchor; + + double repeatAngle = 0; + + // Positions to add or remove in this repeat section. + int repeatPositions = 0; + }; + + class Line { + public: + // The starting point for this line. + Point start; + Point endOrAnchor; + + // Sections of the line that repeat. + std::vector repeats; + + // The number of initial positions for this line. + int positions = 1; + }; private: // Name of the formation pattern. std::string name; - // The positions that define the formation. - std::vector positions; + // Indicates if the formation is rotatable. A value of -1 means not + // rotatable, while a positive value is taken as the rotation angle + // in relation to the full 360 degrees full angle: + // Square and Diamond shapes could get a value of 90, since you can + // rotate such a shape over 90 degrees and still have the same shape. + // Triangles could get a value of 120, since you can rotate them over + // 120 degrees and again get the same shape. + // A value of 0 means that the formation can be rotated in any way and + // still be fine. This could be used for shapes like (perfect) circles + // and for formations where rotation just shouldn't happen. + int rotatable = -1; + // Indicates if the formation is flippable along the longitudinal axis. + bool flippableY = false; + // Indicates if the formation is flippable along the transverse axis. + bool flippableX = false; + // The lines that define the formation. + std::vector lines; }; + + #endif diff --git a/source/FormationPositioner.cpp b/source/FormationPositioner.cpp new file mode 100644 index 000000000000..4d11364bae2d --- /dev/null +++ b/source/FormationPositioner.cpp @@ -0,0 +1,252 @@ +/* FormationPositioner.cpp +Copyright (c) 2019-2022 by Peter van der Meer + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#include "FormationPositioner.h" + +#include "Angle.h" +#include "Body.h" +#include "FormationPattern.h" +#include "Point.h" +#include "Ship.h" + +#include +#include + +using namespace std; + + + +// Initializer based on the formation pattern to follow. +FormationPositioner::FormationPositioner(const Body *formationLead, const FormationPattern *pattern) + : formationLead(formationLead), pattern(pattern), direction(formationLead->Facing()) +{ +} + + + +void FormationPositioner::Step() +{ + // Calculate the direction in which the formation is facing. We perform + // this calculation every step, because we want to act on course-changes + // from the lead ship as soon as they happen. + CalculateDirection(); + + // Calculate the set of positions for the participating ships. This calculation + // can be performed once every 20 game-steps, because the positions are + // relatively stable and slower changes don't impact the formation a lot. + constexpr int POSITIONS_INTERVAL = 20; + if(positionsTimer == 0) + { + CalculatePositions(); + positionsTimer = POSITIONS_INTERVAL; + } + else + positionsTimer--; +} + + + +Point FormationPositioner::Position(const Ship *ship) +{ + Point relPos; + + auto it = shipPositions.find(ship); + if(it != shipPositions.end()) + { + // Retrieve the ships currently known coordinate in the formation. + auto &status = it->second; + + // Register that this ship was seen. + if(status.second != tickTock) + status.second = tickTock; + + // Return the cached position that we have for the ship. + relPos = status.first; + } + else + { + // Add the ship to the set of coordinates. We add it with a default + // coordinate of Point(0,0), it will gets its proper coordinate in + // the next generate round. + shipPositions[ship] = make_pair(relPos, tickTock); + + // Add the ship to the ring. + shipsInFormation.push_back(ship->shared_from_this()); + + // Trigger immediate re-generation of the formation positions (to + // ensure that this new ship also gets a valid position). + positionsTimer = 0; + } + + return formationLead->Position() + direction.Rotate(relPos); +} + + + +// Re-generate the list of (relative) positions for the ships in the formation. +void FormationPositioner::CalculatePositions() +{ + // Run the position iterator for the ships in the formation. + auto itPos = pattern->begin(centerBodyRadius); + + // Run the iterator. + size_t shipIndex = 0; + while(shipIndex < shipsInFormation.size()) + { + // If the ship is no longer valid or not or no longer part of this + // formation, then we need to remove it. + auto ship = shipsInFormation[shipIndex].lock(); + bool removeShip = !ship || !IsActiveInFormation(ship.get()); + + // Lookup the ship in the positions map. + auto itCoor = shipPositions.end(); + if(ship) + itCoor = shipPositions.find(ship.get()); + + // If the ship is not in the overall table or if it was not + // active since the last iteration, then we also remove it. + removeShip |= itCoor == shipPositions.end() || + itCoor->second.second != tickTock; + + // Perform removes if we need to. + if(removeShip) + { + // Remove the ship from the ring. + Remove(shipIndex); + // And remove the ship from the shipsPositions map. + if(itCoor != shipPositions.end()) + shipPositions.erase(itCoor); + } + else + { + // Calculate the new coordinate for the current ship. + Point &shipRelPos = itCoor->second.first; + shipRelPos = *itPos; + if(flippedY) + shipRelPos.Set(-shipRelPos.X(), shipRelPos.Y()); + if(flippedX) + shipRelPos.Set(shipRelPos.X(), -shipRelPos.Y()); + ++itPos; + ++shipIndex; + } + } + + // Switch marker to detect stale/missing ships in the next iteration. + tickTock = !tickTock; +} + + + +void FormationPositioner::CalculateDirection() +{ + // Calculate new direction. If the formationLead is moving, then we use the movement vector, + // otherwise use the facing vector. + Point velocity = formationLead->Velocity(); + Angle desiredDir = velocity.Length() > .1 ? Angle(velocity) : formationLead->Facing(); + + Angle deltaDir = desiredDir - direction; + + // Change the desired direction according to rotational settings if that fits better. + double symRot = pattern->Rotatable(); + if(symRot > 0 && fabs(deltaDir.Degrees()) > symRot / 2) + { + if(deltaDir.Degrees() > 0) + symRot = -symRot; + + while(fabs(deltaDir.Degrees() + symRot) < fabs(deltaDir.Degrees())) + { + desiredDir += Angle(symRot); + deltaDir = desiredDir - direction; + } + } + + // Angle at which to perform longitudinal or transverse mirror instead of turn. + constexpr double MIN_FLIP_TRIGGER = 135.; + + // If we are beyond the triggers for flipping, then immediately go to the desired direction. + if(fabs(deltaDir.Degrees()) >= MIN_FLIP_TRIGGER && + (pattern->FlippableY() || pattern->FlippableX())) + { + direction = desiredDir; + deltaDir = Angle(0.); + if(pattern->FlippableY()) + { + flippedY = !flippedY; + positionsTimer = 0; + } + if(pattern->FlippableX()) + { + flippedX = !flippedX; + positionsTimer = 0; + } + } + else if(symRot != 0) + { + // Turn max 1/4th degree per frame. The game runs at 60fps, so a turn of 180 degrees will take + // about 12 seconds. + constexpr double MAX_FORMATION_TURN = .25; + + deltaDir = Angle(clamp(deltaDir.Degrees(), -MAX_FORMATION_TURN, MAX_FORMATION_TURN)); + + direction += deltaDir; + } +} + + + +// Check if a ship is active in the current formation. +bool FormationPositioner::IsActiveInFormation(const Ship *ship) const +{ + // Ships need to be active, need to have the same formation pattern and + // need to be in the same system as their formation lead in order to + // participate in the formation. + if(ship->GetFormationPattern() != pattern || + ship->IsDisabled() || ship->IsLanding() || ship->IsBoarding()) + return false; + + // TODO: add check if ship is attacking. + // TODO: add check if ship and lead are in the same system. + // TODO: add check for isAssisting. + // TODO: check if we can move many checks to Ship and get an ship->IsStationKeeping() instead. + + // A ship active in the formation should follow the current formationleader + // either through targetShip (for gather/keep-station commands) or through + // the child/parent relationship. + auto targetShip = ship->GetTargetShip(); + auto parentShip = ship->GetParent(); + if((!targetShip || targetShip.get() != formationLead) && + (!parentShip || parentShip.get() != formationLead)) + return false; + + return true; +} + + + +// Remove a ship from the formation (based on its index). The last ship +// in the formation will take the position of the removed ship (if the removed +// ship itself is not the last ship). +void FormationPositioner::Remove(unsigned int index) +{ + if(shipsInFormation.empty()) + return; + + // Move the last element to the current position and remove the last + // element; this will let last ship take the position of the ship that + // we will remove. + if(index < shipsInFormation.size() - 1) + shipsInFormation[index].swap(shipsInFormation.back()); + shipsInFormation.pop_back(); +} diff --git a/source/FormationPositioner.h b/source/FormationPositioner.h new file mode 100644 index 000000000000..19e56de825e2 --- /dev/null +++ b/source/FormationPositioner.h @@ -0,0 +1,95 @@ +/* FormationPositioner.h +Copyright (c) 2019-2022 by Peter van der Meer + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#ifndef FORMATION_POSITIONER_H_ +#define FORMATION_POSITIONER_H_ + +#include "Angle.h" + +#include +#include +#include + +class Body; +class FormationPattern; +class Ship; + + + +// Represents an active formation for a set of spaceships. Assigns each ship +// to a position (Point) in the formation. +class FormationPositioner { +public: + // Initializer based on the formation pattern to follow. + FormationPositioner(const Body *formationLead, const FormationPattern *pattern); + + // Start/reset/initialize for a (new) round of formation position calculations. + void Step(); + + // Get the formation position for the ship given as parameter. If a given ship is + // not participating yet, then it will be added. + Point Position(const Ship *ship); + + +private: + // Re-generate the list of (relative) positions for the ships in the formation. + void CalculatePositions(); + + // Calculate the direction the formation is facing. + void CalculateDirection(); + + // Check if a ship is actually still participating in the current formation(ring). + bool IsActiveInFormation(const Ship *ship) const; + + // Remove a ship from the formation (based on its index). The last ship + // in the formation will take the position of the removed ship (if the removed + // ship itself is not the last ship). + void Remove(unsigned int index); + + +private: + // Lists of ships on the rings. Used when (re)generating positions for the ring. + // The actual positions are stored in shipPositions. + std::vector> shipsInFormation; + // Lookup/cache of the ship coordinates in the formation, its ring-section and + // an indicator if it was seen since last generate loop. + std::map> shipPositions; + + // Timer that controls the (re)generation of ship positions. + int positionsTimer = 0; + + // The scaling factor as we currently have for this formation for the ship or + // other body around which this formation is formed. + double centerBodyRadius = 150; + + // The body around which the formation will be formed and the pattern to follow. + const Body *formationLead; + const FormationPattern *pattern; + + // The formation facing direction. + Angle direction; + + // Settings for flipping/mirroring of the pattern. + bool flippedX = false; + bool flippedY = false; + + // Status variable used to track if ships still participate in the formation. + // TODO: This method of tracking shouldn't be needed; ships themselves should have a state to show what they are doing. + bool tickTock = true; +}; + + + +#endif diff --git a/source/Ship.cpp b/source/Ship.cpp index 78f4170d7fd8..9e128d294da4 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -24,6 +24,7 @@ this program. If not, see . #include "Effect.h" #include "Flotsam.h" #include "text/Format.h" +#include "FormationPattern.h" #include "GameData.h" #include "Government.h" #include "JumpTypes.h" @@ -566,6 +567,8 @@ void Ship::Load(const DataNode &node) description += child.Token(1); description += '\n'; } + else if(key == "formation" && child.Size() >= 2) + formationPattern = GameData::Formations().Get(child.Token(1)); else if(key == "remove" && child.Size() >= 2) { if(child.Token(1) == "bays") @@ -1116,7 +1119,8 @@ void Ship::Save(DataWriter &out) const if(it.second) out.Write("final explode", it.first->Name(), it.second); }); - + if(formationPattern) + out.Write("formation", formationPattern->Name()); if(currentSystem) out.Write("system", currentSystem->Name()); else @@ -3588,6 +3592,13 @@ const set &Ship::GetTractorFlotsam() const +const FormationPattern *Ship::GetFormationPattern() const +{ + return formationPattern; +} + + + void Ship::SetFleeing(bool fleeing) { isFleeing = fleeing; @@ -3660,6 +3671,13 @@ void Ship::SetParent(const shared_ptr &ship) +void Ship::SetFormationPattern(const FormationPattern *formationToSet) +{ + formationPattern = formationToSet; +} + + + bool Ship::CanPickUp(const Flotsam &flotsam) const { if(this == flotsam.Source()) diff --git a/source/Ship.h b/source/Ship.h index 1ef099798de2..be817288f368 100644 --- a/source/Ship.h +++ b/source/Ship.h @@ -43,6 +43,7 @@ class DataNode; class DataWriter; class Effect; class Flotsam; +class FormationPattern; class Government; class Minable; class Phrase; @@ -460,6 +461,8 @@ class Ship : public Body, public std::enable_shared_from_this { std::shared_ptr GetTargetAsteroid() const; std::shared_ptr GetTargetFlotsam() const; const std::set &GetTractorFlotsam() const; + // Pattern to use when flying in a formation. + const FormationPattern *GetFormationPattern() const; // Mark this ship as fleeing. void SetFleeing(bool fleeing = true); @@ -473,6 +476,8 @@ class Ship : public Body, public std::enable_shared_from_this { // Mining target. void SetTargetAsteroid(const std::shared_ptr &asteroid); void SetTargetFlotsam(const std::shared_ptr &flotsam); + // Pattern to use when flying in a formation (nullptr to clear formation). + void SetFormationPattern(const FormationPattern *formation); bool CanPickUp(const Flotsam &flotsam) const; @@ -698,6 +703,7 @@ class Ship : public Body, public std::enable_shared_from_this { std::weak_ptr targetAsteroid; std::weak_ptr targetFlotsam; std::set tractorFlotsam; + const FormationPattern *formationPattern = nullptr; // Links between escorts and parents. std::vector> escorts; diff --git a/source/UniverseObjects.cpp b/source/UniverseObjects.cpp index 899de72c4143..6e4836959c7a 100644 --- a/source/UniverseObjects.cpp +++ b/source/UniverseObjects.cpp @@ -304,7 +304,6 @@ void UniverseObjects::CheckReferences() for(const auto &it : wormholes) if(it.second.Name().empty()) Warn("wormhole", it.first); - // Formation patterns are not serialized, but their usage is. for(auto &&it : formations) if(it.second.Name().empty()) diff --git a/tests/unit/src/test_formationPattern.cpp b/tests/unit/src/test_formationPattern.cpp index 5aec0f7518f8..f19a114a2fa0 100644 --- a/tests/unit/src/test_formationPattern.cpp +++ b/tests/unit/src/test_formationPattern.cpp @@ -1,5 +1,5 @@ /* test_formationPattern.cpp -Copyright (c) 2021 by Peter van der Meer +Copyright (c) 2021-2024 by Peter van der Meer Endless Sky is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -84,8 +84,9 @@ SCENARIO( "Loading and using of a formation pattern", "[formationPattern][Positi FormationPattern emptyFormation; emptyFormation.Load(emptyNode); REQUIRE( emptyFormation.Name() == "Empty"); + double centerBodyRadius = 0.; WHEN( "positions are requested") { - auto it = emptyFormation.begin(); + auto it = emptyFormation.begin(centerBodyRadius); THEN ( "all returned positions are near Point(0,0)" ) { CHECK( Near(*it, Point(0, 0)) ); ++it; @@ -103,7 +104,8 @@ SCENARIO( "Loading and using of a formation pattern", "[formationPattern][Positi tailFormation.Load(tailNode); REQUIRE( tailFormation.Name() == "Tail (px point)"); WHEN( "positions are requested") { - auto it = tailFormation.begin(); + double centerBodyRadius = 0.; + auto it = tailFormation.begin(centerBodyRadius); THEN ( "all returned positions are as expected" ) { CHECK( Near(*it, Point(-100, 0)) ); ++it; @@ -122,17 +124,36 @@ SCENARIO( "Loading and using of a formation pattern", "[formationPattern][Positi CHECK( Near(*it, Point(-800, 0)) ); } } + WHEN( "a centerbody radius is set" ) + { + double centerBodyRadius = 250.; + auto it = tailFormation.begin(centerBodyRadius); + THEN ( "the points in the center are skipped" ) { + CHECK( Near(*it, Point(-300, 0)) ); + ++it; + CHECK( Near(*it, Point(-400, 0)) ); + ++it; + CHECK( Near(*it, Point(-500, 0)) ); + ++it; + CHECK( Near(*it, Point(-600, 0)) ); + ++it; + CHECK( Near(*it, Point(-700, 0)) ); + ++it; + CHECK( Near(*it, Point(-800, 0)) ); + } + } } GIVEN( "a formation pattern loaded in px" ) { auto delta_pxNode = AsDataNode(formation_delta_tail_px); FormationPattern delta_px; delta_px.Load(delta_pxNode); + double centerBodyRadius = 0.; REQUIRE( delta_px.Name() == "Delta Tail (px)" ); WHEN( "positions are requested") { THEN ( "the correct positions are calculated" ) { // No exact comparisons due to doubles, but we check if // the given points are very close to what they should be. - auto it = delta_px.begin(); + auto it = delta_px.begin(centerBodyRadius); REQUIRE( Near(*it, Point(-100, 200)) ); ++it; REQUIRE( Near(*it, Point(100, 200)) ); From 2ee93f1074b2dc6e1974eb6a4be44045eb530121 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Fri, 28 Jun 2024 23:15:49 +0300 Subject: [PATCH 46/75] fix(content): Remnant Side Missions Fix (#10251) --- data/remnant/remnant 2 side missions.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/remnant/remnant 2 side missions.txt b/data/remnant/remnant 2 side missions.txt index 2dbe389942a1..fc6edb41410e 100644 --- a/data/remnant/remnant 2 side missions.txt +++ b/data/remnant/remnant 2 side missions.txt @@ -746,7 +746,7 @@ mission "Remnant: Will Not Someone Please Think Of The Children" personality heroic target marked staying disables plunders system "Vaticanus" ship "Kas'lor Ik 582 (Stranded)" "Fligrookor" - dialog `You have disabled the Kas'lor Ik 582. Time to report back to .` + dialog `You have disabled the Dathnak A'awoj. Time to report back to .` npc government "Korath" personality timid disables plunders swarming @@ -767,5 +767,5 @@ mission "Remnant: Will Not Someone Please Think Of The Children" payment 2500000 conversation `You find Chilia with a relieved look on his face when you land.` - ` "These assignments are typically handled by our most trusted teams, as taking on the larger Korath vessels is a sensitive matter. Most of their larger ships are home to large groups of Korath civilians, including children. I must admit there were concerns you might not be those children's best chance, but our fleet was occupied elsewhere at the time. I am glad I was not wrong in my assessment of you." He signs with a content smile on his face.` + ` "These assignments are typically handled by our most trusted teams, as taking on the larger Korath vessels is a sensitive matter. Most of their larger ships are home to large groups of Korath civilians, including children. I must admit there were concerns you might not be those children's best chance, but our fleet was occupied elsewhere at the time. I am glad I was not wrong in my assessment of you," he signs with a content smile on his face.` ` "We must be careful not to let ignorance of Korath society result in the unnecessary destruction of similar ships. Now that you have shown you can handle these tasks with the proper finesse, you can expect these to turn up in the task board in the future." Chilia then walks away with a confident pace.` From ea8ebed58cc4ef03a859d4ab1b4372e663c0fbbc Mon Sep 17 00:00:00 2001 From: tibetiroka <68112292+tibetiroka@users.noreply.github.com> Date: Fri, 28 Jun 2024 22:27:25 +0200 Subject: [PATCH 47/75] refactor(code): Small DataWriter refactor (#10266) --- source/DataNode.cpp | 25 ++-- source/DataNode.h | 4 + source/DataWriter.cpp | 39 ++++-- source/DataWriter.h | 5 + source/PrintData.cpp | 127 ++++++++++--------- tests/CMakeLists.txt | 1 + tests/unit/src/test_datawriter.cpp | 191 +++++++++++++++++++++++++++++ 7 files changed, 313 insertions(+), 79 deletions(-) create mode 100644 tests/unit/src/test_datawriter.cpp diff --git a/source/DataNode.cpp b/source/DataNode.cpp index 2c22e23c7a3a..d08938d53d52 100644 --- a/source/DataNode.cpp +++ b/source/DataNode.cpp @@ -15,6 +15,7 @@ this program. If not, see . #include "DataNode.h" +#include "DataWriter.h" #include "Logger.h" #include @@ -93,6 +94,14 @@ const vector &DataNode::Tokens() const noexcept +// Add tokens to the node. +void DataNode::AddToken(const std::string &token) +{ + tokens.emplace_back(token); +} + + + // Get the token with the given index. No bounds checking is done. // DataFile loading guarantees index 0 always exists. const string &DataNode::Token(int index) const @@ -262,6 +271,14 @@ bool DataNode::IsBool(const string &token) +// Add a new child. The child's parent must be this node. +void DataNode::AddChild(const DataNode &child) +{ + children.emplace_back(child); +} + + + // Check if this node has any children. bool DataNode::HasChildren() const noexcept { @@ -307,13 +324,7 @@ int DataNode::PrintTrace(const string &message) const { if(&token != &tokens.front()) line += ' '; - bool hasSpace = any_of(token.begin(), token.end(), [](unsigned char c) { return isspace(c); }); - bool hasQuote = any_of(token.begin(), token.end(), [](char c) { return (c == '"'); }); - if(hasSpace) - line += hasQuote ? '`' : '"'; - line += token; - if(hasSpace) - line += hasQuote ? '`' : '"'; + line += DataWriter::Quote(token); } Logger::LogError(line); diff --git a/source/DataNode.h b/source/DataNode.h index 7e40a813fb5f..36c4d791ecc9 100644 --- a/source/DataNode.h +++ b/source/DataNode.h @@ -44,6 +44,8 @@ class DataNode { int Size() const noexcept; // Get all the tokens in this node as an iterable vector. const std::vector &Tokens() const noexcept; + // Add tokens to the node. + void AddToken(const std::string &token); // Get the token at the given index. No bounds checking is done internally. // DataFile loading guarantees index 0 always exists. const std::string &Token(int index) const; @@ -64,6 +66,8 @@ class DataNode { bool IsBool(int index) const; static bool IsBool(const std::string &token); + // Add a new child. The child's parent must be this node. + void AddChild(const DataNode &child); // Check if this node has any children. If so, the iterator functions below // can be used to access them. bool HasChildren() const noexcept; diff --git a/source/DataWriter.cpp b/source/DataWriter.cpp index 493e54572c6e..74ef202b0b52 100644 --- a/source/DataWriter.cpp +++ b/source/DataWriter.cpp @@ -63,6 +63,14 @@ void DataWriter::SaveToPath(const std::string &filepath) +// Get the contents as a string. +string DataWriter::SaveToString() +{ + return out.str(); +} + + + // Write a DataNode with all its children. void DataWriter::Write(const DataNode &node) { @@ -113,7 +121,8 @@ void DataWriter::EndChild() // Write a comment line, at the current indentation level. void DataWriter::WriteComment(const string &str) { - out << indent << "# " << str << '\n'; + out << *before << "# " << str; + Write(); } @@ -128,22 +137,30 @@ void DataWriter::WriteToken(const char *a) // Write a token, given as a string object. void DataWriter::WriteToken(const string &a) +{ + out << *before; + out << Quote(a); + + // The next token written will not be the first one on this line, so it only + // needs to have a single space before it. + before = &space; +} + + + +string DataWriter::Quote(const std::string &a) { // Figure out what kind of quotation marks need to be used for this string. bool hasSpace = any_of(a.begin(), a.end(), [](unsigned char c) { return isspace(c); }); bool hasQuote = any_of(a.begin(), a.end(), [](char c) { return (c == '"'); }); + bool hasBacktick = any_of(a.begin(), a.end(), [](char c) { return (c == '`'); }); // If the token is an empty string, it needs to be wrapped in quotes as if it had a space. hasSpace |= a.empty(); - // Write the token, enclosed in quotes if necessary. - out << *before; + if(hasQuote) - out << '`' << a << '`'; - else if(hasSpace) - out << '"' << a << '"'; + return '`' + a + '`'; + else if(hasSpace || hasBacktick) + return '"' + a + '"'; else - out << a; - - // The next token written will not be the first one on this line, so it only - // needs to have a single space before it. - before = &space; + return a; } diff --git a/source/DataWriter.h b/source/DataWriter.h index 2a2dd7e874ee..61aee6e0f892 100644 --- a/source/DataWriter.h +++ b/source/DataWriter.h @@ -47,6 +47,8 @@ class DataWriter { // Save the contents to a file. void SaveToPath(const std::string &path); + // Get the contents as a string. + std::string SaveToString(); // The Write() function can take any number of arguments. Each argument is // converted to a token. Arguments may be strings or numeric values. @@ -75,6 +77,9 @@ class DataWriter { template void WriteToken(const A &a); + // Enclose a string in the correct quotation marks. + static std::string Quote(const std::string &text); + private: // Save path (in UTF-8). Empty string for in-memory DataWriter. diff --git a/source/PrintData.cpp b/source/PrintData.cpp index bdfadf016034..def1b6e3dfee 100644 --- a/source/PrintData.cpp +++ b/source/PrintData.cpp @@ -17,6 +17,7 @@ this program. If not, see . #include "DataFile.h" #include "DataNode.h" +#include "DataWriter.h" #include "GameData.h" #include "GameEvent.h" #include "LocationFilter.h" @@ -51,7 +52,7 @@ namespace { void PrintItemSales(const Set &items, const Set> &sales, const string &itemNoun, const string &saleNoun) { - cout << itemNoun << ',' << saleNoun << '\n'; + cout << DataWriter::Quote(itemNoun) << ',' << DataWriter::Quote(saleNoun) << '\n'; map> itemSales; for(auto &saleIt : sales) for(auto &itemIt : saleIt.second) @@ -60,9 +61,9 @@ namespace { { if(itemIt.first != ObjectName(itemIt.second)) continue; - cout << '"' << itemIt.first << '"'; + cout << DataWriter::Quote(itemIt.first); for(auto &saleName : itemSales[itemIt.first]) - cout << ',' << '"' << saleName << '"'; + cout << ',' << DataWriter::Quote(saleName); cout << '\n'; } } @@ -72,13 +73,13 @@ namespace { template void PrintSales(const Set> &sales, const string &saleNoun, const string &itemNoun) { - cout << saleNoun << ';' << itemNoun << '\n'; + cout << DataWriter::Quote(saleNoun) << ';' << DataWriter::Quote(itemNoun) << '\n'; for(auto &saleIt : sales) { - cout << '"' << saleIt.first << '"'; + cout << DataWriter::Quote(saleIt.first); int index = 0; for(auto &item : saleIt.second) - cout << (index++ ? ';' : ',') << '"' << ObjectName(*item) << '"'; + cout << (index++ ? ';' : ',') << DataWriter::Quote(ObjectName(*item)); cout << '\n'; } } @@ -86,13 +87,11 @@ namespace { // Take a Set and print a list of the names (keys) it contains. template - void PrintObjectList(const Set &objects, bool withQuotes, const string &name) + void PrintObjectList(const Set &objects, const string &name) { - cout << name << '\n'; - const string start = withQuotes ? "\"" : ""; - const string end = withQuotes ? "\"\n" : "\n"; + cout << DataWriter::Quote(name) << '\n'; for(const auto &it : objects) - cout << start << it.first << end; + cout << DataWriter::Quote(it.first) << endl; } // Takes a Set of objects and prints the key for each, followed by a list of its attributes. @@ -100,14 +99,14 @@ namespace { template void PrintObjectAttributes(const Set &objects, const string &name) { - cout << name << ',' << "attributes" << '\n'; + cout << DataWriter::Quote(name) << ',' << DataWriter::Quote("attributes") << '\n'; for(auto &it : objects) { - cout << '"' << it.first << '"'; + cout << DataWriter::Quote(it.first); const Type &object = it.second; int index = 0; for(const string &attribute : object.Attributes()) - cout << (index++ ? ';' : ',') << '"' << attribute << '"'; + cout << (index++ ? ';' : ',') << DataWriter::Quote(attribute); cout << '\n'; } } @@ -117,7 +116,7 @@ namespace { template void PrintObjectsByAttribute(const Set &objects, const string &name) { - cout << "attribute" << ',' << name << '\n'; + cout << DataWriter::Quote("attribute") << ',' << DataWriter::Quote(name) << '\n'; set attributes; for(auto &it : objects) { @@ -127,13 +126,13 @@ namespace { } for(const string &attribute : attributes) { - cout << '"' << attribute << '"'; + cout << DataWriter::Quote(attribute); int index = 0; for(auto &it : objects) { const Type &object = it.second; if(object.Attributes().count(attribute)) - cout << (index++ ? ';' : ',') << '"' << it.first << '"'; + cout << (index++ ? ';' : ',') << DataWriter::Quote(it.first); } cout << '\n'; } @@ -144,11 +143,13 @@ namespace { { auto PrintBaseShipStats = []() -> void { - cout << "model" << ',' << "category" << ',' << "chassis cost" << ',' << "loaded cost" << ',' << "shields" << ',' - << "hull" << ',' << "mass" << ',' << "drag" << ',' << "heat dissipation" << ',' - << "required crew" << ',' << "bunks" << ',' << "cargo space" << ',' << "fuel" << ',' - << "outfit space" << ',' << "weapon capacity" << ',' << "engine capacity" << ',' << "gun mounts" << ',' - << "turret mounts" << ',' << "fighter bays" << ',' << "drone bays" << '\n'; + cout << "model" << ',' << "category" << ',' << DataWriter::Quote("chassis cost") << ',' + << DataWriter::Quote("loaded cost") << ',' << "shields" << ',' << "hull" << ',' << "mass" << ',' << "drag" << ',' + << DataWriter::Quote("heat dissipation") << ',' << DataWriter::Quote("required crew") << ',' << "bunks" << ',' + << DataWriter::Quote("cargo space") << ',' << "fuel" << ',' << DataWriter::Quote("outfit space") << ',' + << DataWriter::Quote("weapon capacity") << ',' << DataWriter::Quote("engine capacity") << ',' + << DataWriter::Quote("gun mounts") << ',' << DataWriter::Quote("turret mounts") << ',' + << DataWriter::Quote("fighter bays") << ',' << DataWriter::Quote("drone bays") << '\n'; for(auto &it : GameData::Ships()) { @@ -157,10 +158,10 @@ namespace { continue; const Ship &ship = it.second; - cout << '"' << it.first << '"' << ','; + cout << DataWriter::Quote(it.first) << ','; const Outfit &attributes = ship.BaseAttributes(); - cout << '"' << attributes.Category() << '"' << ','; + cout << DataWriter::Quote(attributes.Category()) << ','; cout << ship.ChassisCost() << ','; cout << ship.Cost() << ','; @@ -198,14 +199,15 @@ namespace { auto PrintLoadedShipStats = [](bool variants) -> void { - cout << "model" << ',' << "category" << ',' << "cost" << ',' << "shields" << ',' - << "hull" << ',' << "mass" << ',' << "required crew" << ',' << "bunks" << ',' - << "cargo space" << ',' << "fuel" << ',' << "outfit space" << ',' << "weapon capacity" << ',' - << "engine capacity" << ',' << "speed" << ',' << "accel" << ',' << "turn" << ',' - << "energy generation" << ',' << "max energy usage" << ',' << "energy capacity" << ',' - << "idle/max heat" << ',' << "max heat generation" << ',' << "max heat dissipation" << ',' - << "gun mounts" << ',' << "turret mounts" << ',' << "fighter bays" << ',' - << "drone bays" << ',' << "deterrence" << '\n'; + cout << "model" << ',' << "category" << ',' << "cost" << ',' << "shields" << ',' << "hull" << ',' << "mass" << ',' + << DataWriter::Quote("required crew") << ',' << "bunks" << ',' << DataWriter::Quote("cargo space") << ',' + << "fuel" << ',' << DataWriter::Quote("outfit space") << ',' << DataWriter::Quote("weapon capacity") << ',' + << DataWriter::Quote("engine capacity") << ',' << "speed" << ',' << "accel" << ',' << "turn" << ',' + << DataWriter::Quote("energy generation") << ',' << DataWriter::Quote("max energy usage") << ',' + << DataWriter::Quote("energy capacity") << ',' << DataWriter::Quote("idle/max heat") << ',' + << DataWriter::Quote("max heat generation") << ',' << DataWriter::Quote("max heat dissipation") << ',' + << DataWriter::Quote("gun mounts") << ',' << DataWriter::Quote("turret mounts") << ',' + << DataWriter::Quote("fighter bays") << ',' << DataWriter::Quote("drone bays") << ',' << "deterrence" << '\n'; for(auto &it : GameData::Ships()) { @@ -214,10 +216,10 @@ namespace { continue; const Ship &ship = it.second; - cout << '"' << it.first << '"' << ','; + cout << DataWriter::Quote(it.first) << ','; const Outfit &attributes = ship.Attributes(); - cout << '"' << attributes.Category() << '"' << ','; + cout << DataWriter::Quote(attributes.Category()) << ','; cout << ship.Cost() << ','; auto mass = attributes.Mass() ? attributes.Mass() : 1.; @@ -310,7 +312,7 @@ namespace { if(it.second.TrueModelName() != it.first && !variants) continue; - cout << "\"" << it.first << "\"\n"; + cout << DataWriter::Quote(it.first) << "\n"; } }; @@ -346,14 +348,15 @@ namespace { { auto PrintWeaponStats = []() -> void { - cout << "name" << ',' << "category" << ',' << "cost" << ',' << "space" << ',' << "range" << ',' - << "reload" << ',' << "burst count" << ',' << "burst reload" << ',' << "lifetime" << ',' - << "shots/second" << ',' << "energy/shot" << ',' << "heat/shot" << ',' << "recoil/shot" << ',' - << "energy/s" << ',' << "heat/s" << ',' << "recoil/s" << ',' << "shield/s" << ',' - << "discharge/s" << ',' << "hull/s" << ',' << "corrosion/s" << ',' << "heat dmg/s" << ',' - << "burn dmg/s" << ',' << "energy dmg/s" << ',' << "ion dmg/s" << ',' << "scrambling dmg/s" << ',' - << "slow dmg/s" << ',' << "disruption dmg/s" << ',' << "piercing" << ',' << "fuel dmg/s" << ',' - << "leak dmg/s" << ',' << "push/s" << ',' << "homing" << ',' << "strength" << ',' + cout << "name" << ',' << "category" << ',' << "cost" << ',' << "space" << ',' << "range" << ',' << "reload" << ',' + << DataWriter::Quote("burst count") << ',' << DataWriter::Quote("burst reload") << ',' << "lifetime" << ',' + << "shots/second" << ',' << "energy/shot" << ',' << "heat/shot" << ',' << "recoil/shot" << ',' << "energy/s" << ',' + << "heat/s" << ',' << "recoil/s" << ',' << "shield/s" << ',' << "discharge/s" << ',' << "hull/s" << ',' + << "corrosion/s" << ',' << "heat dmg/s" << ',' << DataWriter::Quote("burn dmg/s") << ',' + << DataWriter::Quote("energy dmg/s") << ',' << DataWriter::Quote("ion dmg/s") << ',' + << DataWriter::Quote("scrambling dmg/s") << ',' << DataWriter::Quote("slow dmg/s") << ',' + << DataWriter::Quote("disruption dmg/s") << ',' << "piercing" << ',' << DataWriter::Quote("fuel dmg/s") << ',' + << DataWriter::Quote("leak dmg/s") << ',' << "push/s" << ',' << "homing" << ',' << "strength" << ',' << "deterrence" << '\n'; for(auto &it : GameData::Outfits()) @@ -363,8 +366,8 @@ namespace { continue; const Outfit &outfit = it.second; - cout << '"' << it.first << '"' << ','; - cout << '"' << outfit.Category() << '"' << ','; + cout << DataWriter::Quote(it.first)<< ','; + cout << DataWriter::Quote(outfit.Category()) << ','; cout << outfit.Cost() << ','; cout << -outfit.Get("weapon capacity") << ','; @@ -436,12 +439,13 @@ namespace { auto PrintEngineStats = []() -> void { - cout << "name" << ',' << "cost" << ',' << "mass" << ',' << "outfit space" << ',' - << "engine capacity" << ',' << "thrust/s" << ',' << "thrust energy/s" << ',' - << "thrust heat/s" << ',' << "turn/s" << ',' << "turn energy/s" << ',' - << "turn heat/s" << ',' << "reverse thrust/s" << ',' << "reverse energy/s" << ',' - << "reverse heat/s" << ',' << "afterburner thrust/s" << ',' << "afterburner energy/s" << ',' - << "afterburner heat/s" << ',' << "afterburner fuel/s" << '\n'; + cout << "name" << ',' << "cost" << ',' << "mass" << ',' << DataWriter::Quote("outfit space") << ',' + << DataWriter::Quote("engine capacity") << ',' << "thrust/s" << ',' << DataWriter::Quote("thrust energy/s") << ',' + << DataWriter::Quote("thrust heat/s") << ',' << "turn/s" << ',' << DataWriter::Quote("turn energy/s") << ',' + << DataWriter::Quote("turn heat/s") << ',' << DataWriter::Quote("reverse thrust/s") << ',' + << DataWriter::Quote("reverse energy/s") << ',' << DataWriter::Quote("reverse heat/s") << ',' + << DataWriter::Quote("afterburner thrust/s") << ',' << DataWriter::Quote("afterburner energy/s") << ',' + << DataWriter::Quote("afterburner heat/s") << ',' << DataWriter::Quote("afterburner fuel/s") << '\n'; for(auto &it : GameData::Outfits()) { @@ -450,7 +454,7 @@ namespace { continue; const Outfit &outfit = it.second; - cout << '"' << it.first << '"' << ','; + cout << DataWriter::Quote(it.first) << ','; cout << outfit.Cost() << ','; cout << outfit.Mass() << ','; cout << outfit.Get("outfit space") << ','; @@ -475,8 +479,9 @@ namespace { auto PrintPowerStats = []() -> void { - cout << "name" << ',' << "cost" << ',' << "mass" << ',' << "outfit space" << ',' - << "energy generation" << ',' << "heat generation" << ',' << "energy capacity" << '\n'; + cout << "name" << ',' << "cost" << ',' << "mass" << ',' << DataWriter::Quote("outfit space") << ',' + << DataWriter::Quote("energy generation") << ',' << DataWriter::Quote("heat generation") << ',' + << DataWriter::Quote("energy capacity") << '\n'; for(auto &it : GameData::Outfits()) { @@ -485,7 +490,7 @@ namespace { continue; const Outfit &outfit = it.second; - cout << '"' << it.first << '"' << ','; + cout << DataWriter::Quote(it.first) << ','; cout << outfit.Cost() << ','; cout << outfit.Mass() << ','; cout << outfit.Get("outfit space") << ','; @@ -509,14 +514,14 @@ namespace { cout << "name" << ',' << "category" << ',' << "cost" << ',' << "mass"; for(const auto &attribute : attributes) - cout << ',' << '"' << attribute << '"'; + cout << ',' << DataWriter::Quote(attribute); cout << '\n'; for(auto &it : GameData::Outfits()) { const Outfit &outfit = it.second; - cout << '"' << outfit.TrueName() << '"' << ','; - cout << '"' << outfit.Category() << '"' << ','; + cout << DataWriter::Quote(outfit.TrueName()) << ','; + cout << DataWriter::Quote(outfit.Category()) << ','; cout << outfit.Cost() << ','; cout << outfit.Mass(); for(const auto &attribute : attributes) @@ -557,7 +562,7 @@ namespace { else if(all) PrintOutfitsAllStats(); else - PrintObjectList(GameData::Outfits(), true, "outfit"); + PrintObjectList(GameData::Outfits(), "outfit"); } void Sales(const char *const *argv) @@ -625,7 +630,7 @@ namespace { else if(attributes) PrintObjectAttributes(GameData::Planets(), "planet"); if(!(descriptions || attributes)) - PrintObjectList(GameData::Planets(), false, "planet"); + PrintObjectList(GameData::Planets(), "planet"); } void Systems(const char *const *argv) @@ -646,7 +651,7 @@ namespace { else if(attributes) PrintObjectAttributes(GameData::Systems(), "system"); else - PrintObjectList(GameData::Systems(), false, "system"); + PrintObjectList(GameData::Systems(), "system"); } void LocationFilterMatches(const char *const *argv) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7b85bfeec60f..61ec10ee37f8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ target_sources(EndlessSkyTests PRIVATE unit/src/test_conditionsStore.cpp unit/src/test_datafile.cpp unit/src/test_datanode.cpp + unit/src/test_datawriter.cpp unit/src/test_dictionary.cpp unit/src/test_distance_calculation_settings.cpp unit/src/test_esuuid.cpp diff --git a/tests/unit/src/test_datawriter.cpp b/tests/unit/src/test_datawriter.cpp new file mode 100644 index 000000000000..57bad08550c4 --- /dev/null +++ b/tests/unit/src/test_datawriter.cpp @@ -0,0 +1,191 @@ +/* test_datawriter.cpp +Copyright (c) 2024 by tibetiroka + +Endless Sky is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +*/ + +#include "es-test.hpp" + +// Include only the tested class's header. +#include "../../../source/DataWriter.h" + +// ... and any system includes needed for the test file. +#include "../../../source/DataNode.h" + +namespace { // test namespace + +// #region mock data +// #endregion mock data + + + +// #region unit tests +TEST_CASE( "DataWriter::Quote", "[datawriter][quote]" ) { + CHECK( DataWriter::Quote("") == "\"\"" ); + CHECK( DataWriter::Quote(" ") == "\" \"" ); + CHECK( DataWriter::Quote("a") == "a" ); + CHECK( DataWriter::Quote("multiple spaces here ") == "\"multiple spaces here \"" ); + CHECK( DataWriter::Quote("\"") == "`\"`" ); + CHECK( DataWriter::Quote("quote and\" space ") == "`quote and\" space `" ); + CHECK( DataWriter::Quote("`") == "\"`\"" ); + CHECK( DataWriter::Quote("long ` text") == "\"long ` text\"" ); +} + +TEST_CASE( "DataWriter::WriteComment", "[datawriter][writecomment]" ) { + DataWriter writer; + GIVEN( "an empty DataWriter" ) { + THEN( "writing a comment is possible" ) { + writer.WriteComment("hello"); + CHECK( writer.SaveToString() == "# hello\n" ); + } + } + GIVEN( "a DataWriter with a partial line" ) { + writer.WriteToken("hello there"); + THEN( "writing a comment is possible" ) { + writer.WriteComment("comment"); + writer.Write("next line"); + CHECK( writer.SaveToString() == "\"hello there\" # comment\n\"next line\"\n" ); + } + } + GIVEN( "a DataWriter with multiple lines" ) { + writer.Write("hello", "there"); + THEN( "writing a comment is possible" ) { + writer.WriteComment("comment"); + CHECK( writer.SaveToString() == "hello there\n# comment\n" ); + } + } + GIVEN( "a DataWriter with indentation" ) { + writer.Write("hello"); + writer.BeginChild(); + THEN( "writing a comment is possible" ) { + writer.Write("there"); + writer.WriteComment("comment"); + writer.Write("after comment"); + writer.EndChild(); + writer.Write("outer"); + CHECK( writer.SaveToString() == "hello\n\tthere\n\t# comment\n\t\"after comment\"\nouter\n" ); + } + THEN( "writing an inline comment is possible" ) { + writer.WriteToken("there"); + writer.WriteComment("comment"); + writer.EndChild(); + CHECK( writer.SaveToString() == "hello\n\tthere # comment\n" ); + } + } + GIVEN( "a DataWriter with multiple levels of indentation" ) { + writer.Write("first"); + writer.BeginChild(); + writer.Write("second"); + writer.BeginChild(); + writer.Write("third"); + THEN( "writing a comment is possible" ) { + writer.WriteComment("comment"); + writer.Write("after comment"); + writer.EndChild(); + writer.Write("second after"); + CHECK( writer.SaveToString() == +R"(first + second + third + # comment + "after comment" + "second after" +)"); + } + THEN( "writing an inline comment is possible" ) { + writer.WriteToken("begin"); + writer.WriteComment("comment"); + writer.EndChild(); + writer.Write("second after"); + CHECK( writer.SaveToString() == +R"(first + second + third + begin # comment + "second after" +)"); + } + } +} + +TEST_CASE( "DataWriter::WriteSorted", "[datawriter][writesorted]" ) { + GIVEN( "a DataWriter" ) { + DataWriter writer; + std::map data; + using InnerType = std::pair; + const auto &sortF = [&](const InnerType *a, const InnerType *b) {return a->second < b->second;}; + const auto &writeF = [&](const InnerType &it){writer.Write(*it.first);}; + AND_GIVEN( "no data" ) { + THEN( "sorting is possible" ) { + WriteSorted(data, sortF, writeF); + CHECK( writer.SaveToString() == "" ); + } + } + AND_GIVEN( "a single data point" ) { + std::string buffer = "1"; + data.emplace(&buffer, 8); + THEN( "sorting is possible" ) { + WriteSorted(data, sortF, writeF); + CHECK( writer.SaveToString() == "1\n" ); + } + } + AND_GIVEN( "multiple data points" ) { + std::string buffer[] = {"1", "6", "3", "4", "5", "2"}; + double nums[] = {1, 6, 3, 4, 5, 2}; + for(int i = 0; i < 6; i++) + data.emplace(buffer + i, nums[i]); + + THEN( "sorting is possible" ) { + WriteSorted(data, sortF, writeF); + CHECK( writer.SaveToString() == "1\n2\n3\n4\n5\n6\n" ); + } + } + } +} + +TEST_CASE( "DataWriter::Write", "[datawriter][write]" ) { + GIVEN( "a DataWriter" ) { + DataWriter writer; + DataNode node; + AND_GIVEN( "a single-level node" ) { + node.AddToken("first"); + node.AddToken("line"); + THEN( "the node can be written" ) { + writer.Write(node); + CHECK( writer.SaveToString() == "first line\n" ); + } + } + AND_GIVEN( "a multi-level node" ) { + node.AddToken("first"); + node.AddToken("line"); + DataNode child(&node); + child.AddToken("second"); + child.AddToken("line"); + DataNode child2(&node); + child2.AddToken("third"); + DataNode child3(&child); + child3.AddToken("inner"); + child.AddChild(child3); + node.AddChild(child); + node.AddChild(child2); + THEN( "the node can be written" ) { + writer.Write(node); + CHECK( writer.SaveToString() == "first line\n\tsecond line\n\t\tinner\n\tthird\n" ); + } + } + } +} +// #endregion unit tests + + + +} // test namespace From 8d82ad2b418fb95a33109826642534c18d25211b Mon Sep 17 00:00:00 2001 From: Nicholas Shanks Date: Sat, 29 Jun 2024 20:10:12 +0100 Subject: [PATCH 48/75] feat(status-messages): Always show destroyed escorts, flotsam collect only if enabled (#10272) --- data/_ui/tooltips.txt | 2 +- source/Engine.cpp | 11 ++++++++--- source/Ship.cpp | 12 ++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/data/_ui/tooltips.txt b/data/_ui/tooltips.txt index c48a7f3584f8..226e5f11458d 100644 --- a/data/_ui/tooltips.txt +++ b/data/_ui/tooltips.txt @@ -1403,7 +1403,7 @@ tip "Alert indicator" `Indicate when hostile ships enter your current system. Audio plays a siren. Visual displays an alert icon by the in-system radar.` tip "Extra fleet status messages" - `Display extra status messages about escorts in your fleet, such as when they are destroyed or scanned.` + `Display extra status messages about escorts in your fleet, such as when they are under-crewed or collect flotsam.` tip "Control ship with mouse" `If active, your flagship will always turn toward your mouse. For activating mouse turning on a key press, see the "Mouse turning (hold)" control.` diff --git a/source/Engine.cpp b/source/Engine.cpp index b019f58de3bd..440639eeb5c6 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -2396,25 +2396,30 @@ void Engine::DoCollection(Flotsam &flotsam) } // Checks for player FlotsamCollection setting + bool collectorIsFlagship = collector == player.Flagship(); if(collector->IsYours()) { const auto flotsamSetting = Preferences::GetFlotsamCollection(); if(flotsamSetting == Preferences::FlotsamCollection::OFF) return; - if(collector == player.Flagship() && flotsamSetting == Preferences::FlotsamCollection::ESCORT) + if(collectorIsFlagship && flotsamSetting == Preferences::FlotsamCollection::ESCORT) return; - if(collector != player.Flagship() && flotsamSetting == Preferences::FlotsamCollection::FLAGSHIP) + if(!collectorIsFlagship && flotsamSetting == Preferences::FlotsamCollection::FLAGSHIP) return; } // Transfer cargo from the flotsam to the collector ship. int amount = flotsam.TransferTo(collector); + // If the collector is not one of the player's ships, we can bail out now. if(!collector->IsYours()) return; + if(!collectorIsFlagship && !Preferences::Has("Extra fleet status messages")) + return; + // One of your ships picked up this flotsam. Describe who it was. - string name = (!collector->GetParent() ? "You" : + string name = (collectorIsFlagship ? "You" : "Your " + collector->Noun() + " \"" + collector->Name() + "\"") + " picked up "; // Describe what they collected from this flotsam. string commodity; diff --git a/source/Ship.cpp b/source/Ship.cpp index 9e128d294da4..51fe7f15ce7e 100644 --- a/source/Ship.cpp +++ b/source/Ship.cpp @@ -3070,7 +3070,7 @@ int Ship::TakeDamage(vector &visuals, const DamageDealt &damage, const G { type |= ShipEvent::DESTROY; - if(IsYours() && Preferences::Has("Extra fleet status messages")) + if(IsYours()) Messages::Add("Your " + DisplayModelName() + " \"" + Name() + "\" has been destroyed.", Messages::Importance::Highest); } @@ -4500,14 +4500,14 @@ void Ship::StepPilot() else if(requiredCrew && static_cast(Random::Int(requiredCrew)) >= Crew()) { pilotError = 30; - if(isYours || (personality.IsEscort() && Preferences::Has("Extra fleet status messages"))) + if(isYours || personality.IsEscort()) { - if(parent.lock()) - Messages::Add("The " + name + " is moving erratically because there are not enough crew to pilot it." - , Messages::Importance::Low); - else + if(!parent.lock()) Messages::Add("Your ship is moving erratically because you do not have enough crew to pilot it." , Messages::Importance::Low); + else if(Preferences::Has("Extra fleet status messages")) + Messages::Add("The " + name + " is moving erratically because there are not enough crew to pilot it." + , Messages::Importance::Low); } } else From 55accab63472e0d33ad57c70d757ce294f28329a Mon Sep 17 00:00:00 2001 From: thewierdnut <9004013+thewierdnut@users.noreply.github.com> Date: Sat, 29 Jun 2024 16:05:56 -0500 Subject: [PATCH 49/75] feat(ui): Add TextArea for auto-scrollable wrapped text support (#9786) Co-authored-by: tibetiroka Co-authored-by: TomGoodIdea <108272452+TomGoodIdea@users.noreply.github.com> --- source/Animate.h | 10 ++ source/CMakeLists.txt | 2 + source/Dialog.cpp | 58 ++++++---- source/Dialog.h | 4 +- source/MissionPanel.cpp | 10 +- source/Panel.cpp | 104 +++++++++++++++++ source/Panel.h | 47 +++++++- source/TextArea.cpp | 245 ++++++++++++++++++++++++++++++++++++++++ source/TextArea.h | 83 ++++++++++++++ source/UI.cpp | 22 ++-- 10 files changed, 548 insertions(+), 37 deletions(-) create mode 100644 source/TextArea.cpp create mode 100644 source/TextArea.h diff --git a/source/Animate.h b/source/Animate.h index e7b721b2b316..c89858a66e1a 100644 --- a/source/Animate.h +++ b/source/Animate.h @@ -41,6 +41,8 @@ class Animate const T &Value() const; // Synonym for Value(). operator const T &() const; + // Returns true if there are no more animation steps pending. + bool IsAnimationDone() const; // Shortcut mathematical operators for convenience. Animate &operator=(const T &v); @@ -113,6 +115,14 @@ Animate::operator const T &() const +template +bool Animate::IsAnimationDone() const +{ + return steps == 0; +} + + + template Animate &Animate::operator=(const T &v) { diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index a27ab293e0d9..6c3b6bd912c4 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -333,6 +333,8 @@ target_sources(EndlessSkyLib PRIVATE TestContext.h TestData.cpp TestData.h + TextArea.cpp + TextArea.h TextReplacements.cpp TextReplacements.h Trade.cpp diff --git a/source/Dialog.cpp b/source/Dialog.cpp index c077f75927e3..89fb115b5097 100644 --- a/source/Dialog.cpp +++ b/source/Dialog.cpp @@ -175,9 +175,6 @@ void Dialog::Draw() okPos.Y() - .5 * font.Height()); font.Draw(okText, labelPos, isOkDisabled ? inactive : (okIsActive ? bright : dim)); - // Draw the text. - text.Draw(textPos, dim); - // Draw the input, if any. if(!isMission && (intFun || stringFun)) { @@ -269,7 +266,8 @@ bool Dialog::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool i if(boolFun) { DoCallback(okIsActive); - GetUI()->Pop(this); + // Use PopThrough because the Dialog has spawned additional panels. + GetUI()->PopThrough(this); } else if(okIsActive || isMission) { @@ -278,11 +276,11 @@ bool Dialog::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool i if(!isOkDisabled) { DoCallback(); - GetUI()->Pop(this); + GetUI()->PopThrough(this); } } else - GetUI()->Pop(this); + GetUI()->PopThrough(this); } else if((key == 'm' || command.Has(Command::MAP)) && system && player) GetUI()->Push(new MapDetailPanel(*player, system)); @@ -330,42 +328,64 @@ void Dialog::Init(const string &message, Truncate truncate, bool canCancel, bool okIsActive = true; isWide = false; - text.SetAlignment(Alignment::JUSTIFIED); - text.SetWrapWidth(Width() - 20); - text.SetFont(FontSet::Get(14)); - text.SetTruncate(truncate); - - text.Wrap(message); + Point textRectSize(Width() - 20, 0); + text = std::make_shared