Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save the owner of the new units in case another action changes the ownership #6943

Merged
merged 4 commits into from
Jul 1, 2020

Conversation

trevan
Copy link
Contributor

@trevan trevan commented Jun 30, 2020

Fixes #6439

In Warcraft War Heroes, units are transformed into "loot" that is then captured by the winner. When the loot is first added to the battle, it is owned by the loser. This causes a series of change commands:

  1. Remove Unit
  2. Add Unit
  3. Change Unit's Owner

When the AI is playing and the user is spectating, the history also sees these change commands and converts them to its local gameData.

The problem occurs when the steps happen in the following manner:

  1. Game - Add Unit
  2. Game - Change Unit's Owner
  3. History - Add Unit
  4. Game - Change Unit's Owner

When 3 runs, it is using the unit that had its owner changed in 2. So the change owner in 4 throws an error because the owner is different from what it is expecting.

This fix stores the original owner when the unit is added. So when 3 runs, it can ensure that the unit has the original owner and not the new one.

Functional Changes

[] New map or map update
[] New Feature
[] Feature update or enhancement
[] Feature Removal
[] Code Cleanup or refactor
[] Configuration Change
[x] Problem fix
[] Other:

Testing

Loaded a save that had the exception and ensured that the exception didn't happen. Also checked that save games still loaded.

Screens Shots

Additional Notes to Reviewer

Release Note

FIX|PlayerOwnerChange exception in Warcraft War Heroes is fixed

@DanVanAtta
Copy link
Member

Nice work digging into this @trevan 👍

I'll try to review this in detail tomorrow if @RoiEXLab does not get to it sooner.

@trevan
Copy link
Contributor Author

trevan commented Jun 30, 2020

The smoke test failed but I don't think this change caused that.

@prastle
Copy link
Contributor

prastle commented Jun 30, 2020

Wow! Welcome to the team! @trevan

@DanVanAtta
Copy link
Member

Perhaps a retry on the smoke-test would be in order (in general), it failed here due to a transient download problem:

Downloading https://services.gradle.org/distributions/gradle-6.5-all.zip
..
Exception in thread "main" javax.net.ssl.SSLProtocolException: Connection reset

this.type = type;
this.name = name;
}

private Map<UUID, String> getUnitPlayerMap(final Collection<Unit> units) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: 'get' methods usually are just 'getters'. If computation is done a different verb could be easier for future maintainers. Perhaps rewording this to buildUnitPlayerMap would help that. WDYT @trevan ?

Copy link
Member

Choose a reason for hiding this comment

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

This could be a moot comment if it turns out we do not need a map.

private Collection<Unit> getUnitsWithOwner(final GameData data) {
final Map<UUID, Unit> uuidToUnits =
units.stream().collect(Collectors.toMap(Unit::getId, unit -> unit));
return unitPlayerMap.entrySet().stream()
Copy link
Member

Choose a reason for hiding this comment

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

If we're iterating over the entry set of the unitPlayerMap, why do we need a map for this? Can we not just iterate over the set of units and avoid the map data structure altogether?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The original code just iterated over the set of units. That's the problem. We need to store the map of unit -> owner outside of the units so that when the owner changes in the unit, it doesn't change the map here.

Copy link
Member

Choose a reason for hiding this comment

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

Cool, I think that probably should be called out to make it clear & obvious. Perhaps a javadoc comment on the map property would do the trick. WDYT?

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 added some documentation and changed the method names. WDYT?

Copy link
Member

Codacy Here is an overview of what got changed by this pull request:

Complexity increasing per file
==============================
- game-core/src/main/java/games/strategy/engine/data/changefactory/AddUnits.java  1
         

See the complete overview on Codacy

Copy link
Member

@DanVanAtta DanVanAtta left a comment

Choose a reason for hiding this comment

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

👍

this.type = type;
this.name = name;
}

private Map<UUID, String> buildUnitOwnerMap(final Collection<Unit> units) {
Copy link
Member

Choose a reason for hiding this comment

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

side-note / big nit, this method could be static. I think we need to try and see if we can share a config that turns on the static analysis warning that methods can be marked as static.

For me it's really helpful as I watch for private methods being static. If they are, I know they do not interact with the class state. If they are not static, I assume they cannot be static and hence interact with class state.

@@ -36,11 +53,36 @@ public Change invert() {
@Override
protected void perform(final GameData data) {
final UnitHolder holder = data.getUnitHolder(name, type);
holder.getUnitCollection().addAll(units);
final Collection<Unit> unitsWithCorrectOwner = buildUnitsWithOwner(data);
holder.getUnitCollection().addAll(unitsWithCorrectOwner);
Copy link
Member

Choose a reason for hiding this comment

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

A test of some sort to verify the right functionality would be a good thing to have. If we change & break this, we'll have the extra safety net. Testing is really good for ensuring correctness, which is a further safety net that would be good to have as well.

@DanVanAtta
Copy link
Member

Waiting for build to complete, good to merge once that finishes.

@trevan
Copy link
Contributor Author

trevan commented Jul 1, 2020

I kind of feel like this is more of a band-aid vs a true fix. I bet I could cause this same type of problem by adding a new unit and then changing its hitpoints or any other edit to the unit.

I think the long term fix is to clone the unit when it is added to this change (and other changes). That way, the original unit can be changed by later changes but the unit in this change object will stay the way it was initially. Is it it ok to try and implement Cloneable on Unit?

@DanVanAtta
Copy link
Member

Would an immutable copy of the unit state work instead of clone? AFAIK clone has its pit-falls, generally a bit error-prone ot use.

@DanVanAtta DanVanAtta merged commit 9402b78 into triplea-game:master Jul 1, 2020
@DanVanAtta
Copy link
Member

DanVanAtta commented Jul 1, 2020

IMO changes should generally contain immutable data. If we maintained a stack of change data, it could be a really good avenue for having better save games. We could serialize the change stack and replay them on a base game data instead of serializing a game data. That kind of change I think would be a big breakthrough for compatibility.

@trevan
Copy link
Contributor Author

trevan commented Jul 1, 2020

That should work as well, though I'm not entirely sure how to do that in a non-clone manner. Is there something like that in the code base already?

@DanVanAtta
Copy link
Member

  • Creating a copy-constructor would do it.
  • Keeping the original data immutable is good.
  • Eventually we won't be wiring a gameData reference to everything (which we only do for save-game), that'll make it easier for the data to be immutable.
  • We can use a state tracking inner object, so you only need to copy that state (similar to serialization proxy pattern)
  • 'lombok' 'toBuilder' is a decent way to get a copy-constructor for free: https://projectlombok.org/features/Builder

Overall I think the band-aid is probably fine for now until we redo how game serialization is done.

@trevan trevan deleted the player-owner-change-error branch July 2, 2020 04:46
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.

2.0.19225: PlayerOwnerChange#perform:55 - IllegalStateException
3 participants