Skip to content

feat: sync away team assignments - #295

Merged
netniV merged 11 commits into
STFC-Mod:devfrom
blevouschurverantunt:feature/sync-away-assignments
Oct 3, 2026
Merged

netniV merged 11 commits into
STFC-Mod:devfrom
blevouschurverantunt:feature/sync-away-assignments

Conversation

@blevouschurverantunt

@blevouschurverantunt blevouschurverantunt commented Sep 19, 2026 •

Copy link
Copy Markdown

Summary

Adds Away Team data to the sync feature so consumers can tell which officers are currently away and until when. Today processors::jobs() skips JOBTYPE_AWAYASSIGNMENT and the dispatcher ignores the AwayAssignments* entity groups, so a crew-planning tool will happily recommend an officer who is off on a 2-day away mission.

Everything is additive, read-only, reuses the existing ProcessResultInternal dispatcher (no new hooks), and is gated like every other data type.

What changes

  • New sync option away_assignments ([sync] and per-target, same semantics/defaults as the other data flags) → new data type away_assignment. It is one new row in SyncOptions, so config.cc needs no changes.

  • away_assignment events, built by one shared helper (away_assignment_event) so both sources share the same de-dup state and slot-ordered officer_ids:

    • Entity group AwayAssignmentsList (111, AwayAssignmentUserListResponse) — sent when the Away Teams screen is opened and after collecting a mission.
    • Entity group AwayAssignmentsInstance (113, a bare AwayAssignmentInstance) — sent when an assignment is started: first with state=1 (READYTOSTART), officers already chosen and an empty job_uuid, then ~1.5s later with state=2 (INPROGRESS) and job_uuid set.

    Example event:

    {"type":"away_assignment","aid":2814298639311718967,"template_id":4186662802,"state":2,
     "officer_ids":[3479870516,440622131,2520801863],"job_uuid":"60f7…264b","duration":43200,"rarity":2}

    officer_ids is emitted in slot order (the proto map iterates in arbitrary order). Events are only emitted when the instance's observable state changed since it was last sent. When an instance disappears from the list a {"type":"collected_away_assignment","aid":…} event is sent and its state is pruned, mirroring completed_job.

  • job events now include JOBTYPE_AWAYASSIGNMENT with {"aid": …}, so start_time/duration flow through the existing job / completed_job events (gated by the existing jobs option) for the instance with the matching aid.

  • All 10 localized example configs get the new key (first entry of Subgroup: Data, alphabetical). The da/de/es/fr/nl/ru comments are best-effort translations in each file's existing style — corrections welcome.

Officer events (processors::officers()) are untouched by this PR — away-assignment status is not mixed into them. A prior revision added an away_assignment_id field there, but slot order isn't available in that hook, so instead of shipping an event that loses it, assignment starts are now reported directly via group 113 above.

Verified against the live client (Windows, probe build, 2026-09-20)

  • Group 111 parses as AwayAssignmentUserListResponse and re-serialises byte-exact; officerIds is slot index → officer id. Sent when the Away Teams screen is opened and in the response to collecting a mission (collected instance appears once with state 5, then drops out → collected_away_assignment). Lists were always complete, so pruning is safe.
  • Group 113 parses as a bare AwayAssignmentInstance and re-serialises byte-exact (285/285 and 317/317 bytes on the two captures); cross-parsing the same bytes as AwayAssignmentUserListResponse yields 0 instances, confirming it is not a wrapped list. It fires when an assignment is started, twice: state=1 (READYTOSTART) with officerIds already populated and an empty jobUuid, then ~1.5s later state=2 (INPROGRESS) with jobUuid set — the second one arrives in the same server response as the Jobs and Officers groups. It was not seen at login, when opening the Away Teams screen, or on collect (those use the 111 list).
  • The option is read from [sync], inherited per target, and written to community_patch_runtime.vars like the other data flags (it goes through the same SyncOptions loop).
  • Not handled on purpose: group 110 (static refresh settings; seen once at login) and group 112 (never observed).

Consumers only need away_assignments to know who is away (the state-2 event arrives at the moment an assignment starts); enable jobs as well for the exact start_time/duration, from the job event whose aid matches.

Note: aid is an int64 above 2^53, emitted as a JSON number like the other ids in the sync stream; JavaScript consumers should be aware of precision.

Checks

  • Fork CI on this exact commit: build-win, build-mac (arm64), build-mac (x86_64), package-mac, Validate example configs — all green.
  • git diff --check clean, LF only, clang-format applied to the changed lines only.
  • No changes to CHANGELOG, README, version.h, or xmake-requires.lock.

🤖 Generated with Claude Code

Add SyncConfig::Type::AwayAssignments as the first enumerator (mirrors
Jobs/Missions/Traits naming), a corresponding away_assignments member
(default false, matching every other sync data flag), and its
SyncOptions row so TOML loading, per-target overrides, runtime.vars
write-back and JSON type serialization all pick it up automatically.
config.cc needs no changes since everything iterates SyncOptions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add processors::away_assignments_list (EntityGroup 111,
AwayAssignmentUserListResponse) and processors::away_assignment_instance
(EntityGroup 113, bare AwayAssignmentInstance), following the same
two-processors-plus-shared-helper split used for entity_slots /
entity_slots_data + single_slot_locked, since no entity group carries a
single-instance wrapper here.

away_assignment_event() builds the sync event for one instance and only
emits it when a hash of its observable fields changes, tracked in the
new trackers::away_assignment_states map (mirrors slot_states). The
list processor also prunes ids that drop out of the response and emits
a collected_away_assignment event for each, mirroring how jobs() prunes
completed jobs.

Wire both entity groups into HandleEntityGroup's dispatcher as the
first (alphabetical) block, gated by the new away_assignments option.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jobs(): add JOBTYPE_AWAYASSIGNMENT so away-team jobs flow through the
existing job/completed_job events with an "aid" pointing at the
instance, in the same style as the other job-type cases.

officers(): surface Officer.awayAssignmentInstanceId as
"away_assignment_id" on the officer event. The officer de-dup state is
a struct local to officers() (kept separate from the shared
RankLevelShardsState used by forbidden_tech) so this extra field
doesn't change forbidden tech's comparison at all, while still causing
an officer to re-emit if only their away assignment changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add the away_assignments key as the first entry of the [sync]
"Subgroup: Data" block in all 10 localized example config files,
matching the existing comment/blank-line rhythm of the other data
keys. da/de/es/fr/nl/ru translations follow each file's own missions
line pattern and are best-effort; tlh follows the file's established
"Do you want Daq  sync X De'?" template. The unsuffixed pointer file
and [sync.targets.*] example blocks are intentionally left untouched,
matching how every other sync data key (e.g. traits) is handled there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
officerIds is a protobuf map, whose iteration order does not match
slot order (observed e.g. 2=.., 0=.., 1=..). Sort by slot index before
emitting officer_ids so the array, and the de-dup hash built from it,
are stable run to run.

Remove the AwayAssignmentsInstance (group 113) processor and dispatch
case: it was never observed on the wire during probing (not at login,
not on opening the screen, not on collect), so its payload shape is
unverified and we should not ship unverified parsing. AwayAssignmentsList
(group 111) is the only observed source of away assignment sync data
and remains the single caller of away_assignment_event(), whose
comment now states plainly that the caller must hold
trackers::away_assignment_states_mtx.

Also runs clang-format over the touched line ranges in config.h and
sync.cc to keep the branch's own added/changed lines clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@netniV netniV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes look good. @lightbull-stfc may want to weigh in on whether we need anything more or compatibility for Spocks Club.

@lightbull-stfc lightbull-stfc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's explore if there's a way to emit updates to away_assignments at a point when an assignment is started (in addition to when the whole list is refreshed) to avoid using officer updates for that.

Comment thread mods/src/patches/parts/sync.cc Outdated
{"level", officer.level()},
{"shard_count", officer.shardcount()}});
{"shard_count", officer.shardcount()},
{"away_assignment_id", officer.awayassignmentinstanceid()}});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This shifts the burden to the sync target to piece back the data together. Slot order is unavailable in this hook and is thus lost. I would prefer to remove it and emit a started assignment through other means.

Comment thread mods/src/patches/parts/sync.cc Outdated
{"duration", instance.parameters().duration()},
{"rarity", instance.rarity()}};

const auto state_value = static_cast<size_t>(std::hash<json>{}(event));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant static_cast

Comment thread mods/src/patches/parts/sync.cc Outdated
Comment on lines +1524 to +1552
@@ -1446,7 +1548,8 @@ static void officers(std::unique_ptr<std::string>&& bytes)
std::scoped_lock lk(officer_states_mtx);

for (const auto& officer : response.officers()) {
const RankLevelShardsState officer_state{officer.rankindex(), officer.level(), officer.shardcount()};
const OfficerState officer_state{officer.rankindex(), officer.level(), officer.shardcount(),
officer.awayassignmentinstanceid()};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would prefer not to have Away Assignments mixed with officers. For users to benefit from this feature addition, they'd currently have to enable away_assignments, officers, and jobs options in the config.

Address sync maintainer review feedback on PR STFC-Mod#295:

- Revert processors::officers() to be byte-identical to dev. Away
  assignment status stays off officer events; slot order isn't
  available in that hook, so instead of losing it we emit assignment
  starts through a dedicated event (see below).
- Drop a redundant static_cast in away_assignment_event(): std::hash
  already returns size_t, matching away_assignment_states's value type.
- Restore processors::away_assignment_instance for EntityGroup 113
  (AwayAssignmentsInstance), parsed as a bare AwayAssignmentInstance and
  routed through the existing away_assignment_event()/
  away_assignment_states_mtx so list and instance updates share one
  de-dup state and the slot-ordered officer_ids. Live capture against
  the Windows client confirms group 113 fires when an assignment is
  started: first at state=1 (READYTOSTART) with officerIds already
  populated and an empty jobUuid, then ~1.5s later at state=2
  (INPROGRESS) with jobUuid set. Wired into HandleEntityGroup's
  dispatcher right after the existing AwayAssignmentsList case, gated
  by the same away_assignments option.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blevouschurverantunt

Copy link
Copy Markdown
Author

Thanks for the review — all addressed in 6bd1893:

  • The officer change is dropped entirely; officers() is identical to dev again.
  • The redundant cast is gone.
  • Start-time emission: I captured a start with groups 110–113 logged. The server sends group 113 (AwayAssignmentsInstance, a bare AwayAssignmentInstance that re-serialises byte-exact) when an assignment is started — once with state 1 and the chosen officers, then ~1.5 s later with state 2 and the job uuid. It doesn't appear at login, when opening the Away Teams screen, or on collect, which is why I hadn't seen it before. It now goes through the same helper as the 111 list (shared de-dup state, slot-ordered officer_ids), so consumers only need away_assignments. With this build, a started assignment reached my sync target as a state-2 away_assignment event about a second after the job's start_time.

The PR description is updated with the details. Fork CI is green on this exact commit (Windows, both macOS archs, package, config validation); the workflows here still need a maintainer's approval to run.

@netniV netniV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the update. Unfortunately, there are now conflicts with this branch. Could you resolve those so myself and @lightbull-stfc can review once more.

…signments

# Conflicts:
#	mods/src/config.h
#	mods/src/patches/parts/sync.cc

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blevouschurverantunt

Copy link
Copy Markdown
Author

Conflicts resolved by merging the current dev into the branch (1db8a05). The Haven sync and this change now sit side by side: both options in SyncOptions, both dispatcher blocks, no changes to either feature's behaviour. Builds clean locally and on my fork's CI. Ready for another look.

@netniV

netniV commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

I have approved, but I'll let @lightbull-stfc have the final say.

…signments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Client build 267 (2026-09-29) delivers battle result headers as entity
group 219 (BattleResultHeadersResponse) and owned ships as group 251
(PlayerShipsResponse) instead of the Json group.

Implement the 219 processor and add a 251 processor, sharing helpers with
the Json handlers. Add the client's EntityGroup values 242-252, and declare
only the proto fields that are read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…assignments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…signments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blevouschurverantunt

Copy link
Copy Markdown
Author

Conflicts resolved again by merging the current dev (now including #315) into the branch (b8c53b1). The diff against dev is unchanged: the away-assignment feature only. Fork CI green on this exact commit.

@netniV
netniV merged commit 1c2e70e into STFC-Mod:dev Oct 3, 2026
6 checks passed
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.

3 participants