Skip to content

Ships: Fix crash when doing expedition between adjacent harbor places#1895

Draft
Noseey wants to merge 11 commits intoReturn-To-The-Roots:masterfrom
Noseey:fix_ship_stuck_crash
Draft

Ships: Fix crash when doing expedition between adjacent harbor places#1895
Noseey wants to merge 11 commits intoReturn-To-The-Roots:masterfrom
Noseey:fix_ship_stuck_crash

Conversation

@Noseey
Copy link
Contributor

@Noseey Noseey commented Feb 17, 2026

Related Issue:
Closes #1784

Changes Summary
This fixes a crash when expeditions are done between harbor places that are directly adjacent to another (e. g. on autogenerated maps), so the ship doesn't need to move at all for reaching them and thus its route_ is empty. For more details, see related issue.

Note:
@Flamefire: Like discussed, could you add also a test for this if possible?

Copy link
Member

@Flamefire Flamefire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into adding a test, generally I think this works but might miss the notification, see comment. It might look odd though: Player clicks "next" and immediately receives a notification about a waiting ship.
But I guess that's ok and rate enough. Better than missing it and having a ship idle.
Or what do you think?

About the style: Compare those 3:

if (foo)
  return x;
else
  bar():
return y;
if (foo)
  return x;
else {
  bar():
  return y;
}
if (foo)
  return x;
bar():
return y;

I find the last one the clearest.

Comment on lines 520 to 526
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check exists already at the top of the function, which got me confused.

I think the only situation would be a route recalculation by one of the 2 FindShipPathToHarbor
So either put a route_.empty() check there after curRouteIdx = 0; or something like:

Suggested change
// Already arrived? (e. g. adjacent harbor)
if(curRouteIdx == route_.size())
return Result::GoalReached;
else
StartDriving(route_[curRouteIdx++]);
return Result::Driving;
// FindShipPathToHarbor above found we arrived already?
if(route_.empty())
return Result::GoalReached;
RTTR_Assert(curRouteIdx < route_.size();
StartDriving(route_[curRouteIdx++]);
return Result::Driving;

(the else was also misleading as it can be omitted after return and looked like return Result::Driving; would always be executed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check exists already at the top of the function, which got me confused.

Yes, in the end it is just another "are we there yet?" check , just after the route recalculations. I found this to be more in line with the rest of the code and since curRouteIdx is set to 0 in these cases, it would also work for empty routes in my opinion. But I can change it back to an check against it being empty.

About the else, I agree, that was not necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My issue as I was reading the function was: This check already exists, why do it again? And what is it about adjacent harbor? DriveToHarbourPlace isn't only called from a harbor so without the background knowledge of this issue it might be confusing.

Only after checking all uses of route_ I remembered that FindShipPathToHarbor changes it's parameter and wanted to keep that knowledge.

Maybe:

Suggested change
// Already arrived? (e. g. adjacent harbor)
if(curRouteIdx == route_.size())
return Result::GoalReached;
else
StartDriving(route_[curRouteIdx++]);
return Result::Driving;
// Already arrived after possibly recalculating route (FindShipPathToHarbor)
if(curRouteIdx == route_.size())
return Result::GoalReached;
RTTR_Assert(curRouteIdx < route_.size();
StartDriving(route_[curRouteIdx++]);
return Result::Driving;

The other idea was to have the check local to where it matters: Above

-curRouteIdx = 0;
+curRouteIdx = 0;
+if(route_.empty())
+    return Result::GoalReached;

And then have only RTTR_Assert(curRouteIdx < route_.size(); here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably haven't taken into account the history of the example map when adding this additional check. For the already stuck ships (due to them driving regardless the access to routes_ was OOB), the curRouteIdx was 1.

With a proper check in place in noShip::ContinueExpedition, this should no longer be possible. I would say, just to be save, we change the check on the top of this function to >= if(curRouteIdx >= route_.size()) and remove the additional check at the bottom. What do you say?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be save

When curRouteIdx > route_.size() we already had an OOB and things are likely damaged already. So that situation cannot happen in a state we can recover from anyways.
It most I'd add an RTTR_Assert(curRouteIdx < route_size()) when == failed, i.e. not returned

So while I appreciate being careful, accounting for a situation that already caused fatal issues is a bit to much for me.

Comment on lines 567 to 574
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading indentation, and check can be against "empty"
And we don't need to set the state (see check at top)

Suggested change
if(curRouteIdx == route_.size())
{
state = State::ExpeditionWaiting;
return;
} else
state = State::ExpeditionDriving;
StartDriving(route_[curRouteIdx++]);
if(!route_.empty())
{
state = State::ExpeditionDriving;
StartDriving(route_[curRouteIdx++]);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we need to handle the "arrived" as an event to e.g. notify the player. Maybe simply:

Suggested change
if(curRouteIdx == route_.size())
{
state = State::ExpeditionWaiting;
return;
} else
state = State::ExpeditionDriving;
StartDriving(route_[curRouteIdx++]);
state = State::ExpeditionDriving;
HandleState_ExpeditionDriving();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you misunderstood: Simply replace the original StartDriving by HandleState_ExpeditionDriving without checking the route-index/size
This is already handled by HandleState_ExpeditionDriving so we can avoid the work here which is also very clean: Advance the state and call the associated state handler instead of duplicating some of its code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, so that the check is not needed here either.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where else this is used but I'd say an empty route is valid: It just doesn't change the current point, i.e. *dest = pos

Or would that cause issues anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the DriveToHarbourPlace, it seems to be also used in two functions for checking the Trade Route, being TradeRoute::GetNextDir (which checks the path route size before calling the function) and TradePathCache::pathExists, which I need check further.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked loosely and found:

if(world.CheckTradeRoute(curPos, path.route, curRouteIdx, player))
nextDir = path.route[curRouteIdx];

So when this returns true we expect route[pos] to be valid, which would not be the case when route is empty.
However RTTR_Assert(pos < route.size()); already assumes we have a valid index, as a precondition. And indeed we cannot have pos >= size and hence empty is never true, see the other discussion about handling situations after OOB access.
Hence we can remove this and ensure our assumption/assertion holds.

@Flamefire
Copy link
Member

Amazing how after all the changes to the changes and discussion the fix is a single line 😄
Thanks for the productive dialog!

I'll try to write a test in the next couple days to cover this. Did you try the original save/replay?

@Noseey
Copy link
Contributor Author

Noseey commented Feb 18, 2026

I'll try to write a test in the next couple days to cover this. Did you try the original save/replay?

Sounds good!
Yes, I did check it with the provided savegames in both debug (commented out asserts) and release build:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ships get stuck on landing spot behind their first landing spot, then building harbor on first spot the game crashes

2 participants

Comments