diff --git a/bridge/sds/SdsMountProbe.java b/bridge/sds/SdsMountProbe.java index 775f74b..72aba8d 100644 --- a/bridge/sds/SdsMountProbe.java +++ b/bridge/sds/SdsMountProbe.java @@ -213,6 +213,7 @@ public final class SdsMountProbe { predicate(out, argv[0], pair[0], pair[1]); } emitted(out, argv[0]); + target(out, argv[0]); for (String[] pair : new String[][] { {"Badger (C) Tracked Transport A", "Clan Heavy Foot Infantry Ebon Keshik Point"}, {"Badger (C) Tracked Transport A", "Fenrir Battle Armor [ERML](Sqd5)"}, @@ -497,6 +498,57 @@ public final class SdsMountProbe { : (double) carrier.getRunMP() / (double) cargo.getRunMP()); } + /** + * Does a bare `LOAD` step name anything to load? + * + *
A route is a list of step names, so `SdsClient.buildPath` calls + * `addStep(MoveStepType)` and never the `(MoveStepType, Targetable)` + * overload. In a match the platoon walked into the carrier's hex and was + * not loaded, so this asks what the step it sent actually carried. + */ + static void target(PrintStream out, String scenario) throws Exception { + MekSummaryCache cache = MekSummaryCache.getInstance(); + megamek.common.scenario.Scenario sc = + new megamek.common.scenario.ScenarioLoader(new java.io.File(scenario)).load(); + Game game = (Game) sc.createGame(); + game.setPhase(megamek.common.enums.GamePhase.MOVEMENT); + MekSummary cm = cache.getMek("Badger (C) Tracked Transport A"); + MekSummary gm = cache.getMek("Motorized Infantry Dark Caste Bandits"); + Entity carrier = new MekFileParser(cm.getSourceFile(), cm.getEntryName()).getEntity(); + Entity cargo = new MekFileParser(gm.getSourceFile(), gm.getEntryName()).getEntity(); + megamek.common.board.Coords at = new megamek.common.board.Coords(3, 3); + for (Entity e : new Entity[] {carrier, cargo}) { + e.setGame(game); + e.setOwner(game.getPlayersList().get(0)); + e.setDeployed(true); + e.setFacing(0); + e.setDone(false); + } + carrier.setId(9501); + cargo.setId(9502); + carrier.setPosition(at); + cargo.setPosition(at.translated(3)); + game.addEntity(carrier); + game.addEntity(cargo); + + for (boolean named : new boolean[] {false, true}) { + megamek.common.moves.MovePath path = + new megamek.common.moves.MovePath(game, cargo); + path.addStep(megamek.common.enums.MoveStepType.FORWARDS); + if (named) { + path.addStep(megamek.common.enums.MoveStepType.LOAD, carrier); + } else { + path.addStep(megamek.common.enums.MoveStepType.LOAD); + } + megamek.common.moves.MoveStep last = path.getLastStep(); + out.printf("target named=%-5s legal=%-5s target=%s%n", + named, path.isMoveLegal(), + last == null || last.getTarget(game) == null + ? "null" + : last.getTarget(game).getDisplayName().replace(' ', '_')); + } + } + private SdsMountProbe() { } } diff --git a/crates/sds-core/src/tactic.rs b/crates/sds-core/src/tactic.rs index e2d1b2c..1823757 100644 --- a/crates/sds-core/src/tactic.rs +++ b/crates/sds-core/src/tactic.rs @@ -883,12 +883,19 @@ impl Tactic for Embark { const TARGETING: Targeting = ENEMY; const GAP: Option<&'static str> = Some( "no feature measures distance to a *particular* friendly unit. \ - `cohesion` is distance to the force as a whole, which is the carrier \ - only while the force is small - with a lance around it, a passenger \ - pulled toward the centroid is not thereby pulled toward its ride. \ - That is the same shape of gap `Control` names for an arbitrary hex: \ - every distance in the basis is to something the board or the force \ - defines, and one named unit is not one of those. See \ + `cohesion` is distance to the force as a whole, which coincides with \ + the carrier only while the force is small - with a lance around it, a \ + passenger pulled toward the centroid is not thereby pulled toward its \ + ride. \ + \ + **This is the same gap `Control` names for an arbitrary hex, and two \ + instances make it a category:** every distance in the basis - \ + `enemy_nearest`, `enemy_centroid`, `cohesion`, `edge_distance`, \ + `cover_distance` - is to something the board or the force defines, \ + and one named unit is not one of those. \ + \ + `Spot` was expressible in existing columns entirely, so this is the \ + first tactic to need something the basis cannot say. See \ `plan/mounting.md`.", ); diff --git a/plan/mounting.md b/plan/mounting.md index 6073d48..1f82a11 100644 --- a/plan/mounting.md +++ b/plan/mounting.md @@ -71,7 +71,17 @@ there is no route to append `LOAD` to. ## Open - [ ] **What a carried unit is worth**, per the trade above. -- [ ] **Whether the server performs the load, and what a carried unit may do.** +- [ ] **A route cannot name what to load, so the load never happens.** Answered, + and it is a protocol gap rather than a generation one. A route is a list + of step names, so `SdsClient.buildPath` calls `addStep(MoveStepType)` and + never the `(MoveStepType, Targetable)` overload that names the carrier. + Measured: a bare `LOAD` step reports `target=null` and the same step given + the carrier reports it by name - **and both are `isMoveLegal` true**, + which is why nothing caught it. In a match the platoon's menu narrowed to + the single mount candidate, it chose it, it walked into the carrier's hex, + and `carriedBy` stayed empty. Fixing it means a step that can carry a + target id. +- [ ] **What a carried unit may do**, once one can be carried. Both are only observable in a match. `TWGameManager.loadUnit` sends a packet and throws without a live `Server`, so unlike motive damage this cannot be driven headlessly - and `Entity.load` is only half the diff --git a/scenarios/tactics/mount-probe.mms b/scenarios/tactics/mount-probe.mms index 4f0a1b7..c28810a 100644 --- a/scenarios/tactics/mount-probe.mms +++ b/scenarios/tactics/mount-probe.mms @@ -10,11 +10,12 @@ # have `MekArms`, `ClampMountMek`, `RoofRack` and `TankTrailerHitch` and carry # no infantry at all. 286 designs have an `InfantryCompartment`; most do not. # -# The passenger is a battle armour squad rather than a foot platoon because a -# platoon runs 1: deployment puts the two several hexes apart and a unit that -# closes one hex a turn cannot reach a carrier before the match is decided. A -# squad runs 4 and the ride is still worth 2.3 times its own pace, so the test -# is about mounting rather than about walking. +# The passenger is motorised rather than on foot because a foot platoon runs 1: +# deployment puts the pair several hexes apart and a unit closing one hex a turn +# cannot reach a carrier before the match is decided. This one runs 3 and the +# ride is still worth three times its own pace, so the test is about mounting +# rather than about walking. A battle armour squad was tried first and its own +# candidate generation is too slow to finish a match here. # # MegaMek Data (C) 2025 by The MegaMek Team is licensed under CC BY-NC-SA 4.0. # https://creativecommons.org/licenses/by-nc-sa/4.0/ @@ -28,5 +29,5 @@ Factions=North,South Location_North=N Location_South=S Unit_North_1=Badger (C) Tracked Transport A,North 1,4,5 -Unit_North_2=Fenrir Battle Armor [ERML](Sqd5),North 2,4,5 +Unit_North_2=Motorized Infantry Dark Caste Bandits,North 2,4,5 Unit_South_1=Locust LCT-1V,South 1,4,5