Skip to content
This repository was archived by the owner on May 10, 2026. It is now read-only.

Commit c0fe5af

Browse files
authored
Merge pull request #160 from ggmolly/fix/island-achievement-pair
feat: add island achievement sync and claim handlers
2 parents beadbe0 + bab6250 commit c0fe5af

8 files changed

Lines changed: 870 additions & 1 deletion
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package answer
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"sort"
7+
"strconv"
8+
9+
"github.com/jackc/pgx/v5"
10+
"google.golang.org/protobuf/proto"
11+
12+
"github.com/ggmolly/belfast/internal/connection"
13+
"github.com/ggmolly/belfast/internal/db"
14+
"github.com/ggmolly/belfast/internal/orm"
15+
"github.com/ggmolly/belfast/internal/protobuf"
16+
)
17+
18+
const (
19+
islandAchievementCategory = "ShareCfg/island_achievement.json"
20+
islandAchievementCategoryLC = "sharecfgdata/island_achievement.json"
21+
22+
islandAchievementClaimOK = uint32(0)
23+
islandAchievementClaimInvalid = uint32(1)
24+
islandAchievementClaimState = uint32(2)
25+
islandAchievementClaimPersist = uint32(3)
26+
27+
islandAchievementMaxSyncEvents = 200
28+
)
29+
30+
type islandAchievementConfig struct {
31+
ID uint32 `json:"id"`
32+
TargetType uint32 `json:"target_type"`
33+
TargetValue uint32 `json:"target_value1"`
34+
TargetNum uint32 `json:"target_num"`
35+
AwardDisplay [][]uint32 `json:"award_display"`
36+
AwardRaw json.RawMessage `json:"award"`
37+
}
38+
39+
type islandAchievementEventKey struct {
40+
EventType uint32
41+
EventArg uint32
42+
}
43+
44+
type islandAchievementSyncRule struct {
45+
MaxValue uint32
46+
}
47+
48+
func IslandClaimAchievementAward(buffer *[]byte, client *connection.Client) (int, int, error) {
49+
var payload protobuf.CS_21050
50+
if err := proto.Unmarshal(*buffer, &payload); err != nil {
51+
return 0, 21051, err
52+
}
53+
54+
response := &protobuf.SC_21051{Result: proto.Uint32(islandAchievementClaimInvalid), DropList: []*protobuf.DROPINFO{}}
55+
if err := ensureCommanderLoaded(client, "Island/AchievementClaim"); err != nil {
56+
response.Result = proto.Uint32(islandAchievementClaimPersist)
57+
return client.SendMessage(21051, response)
58+
}
59+
60+
achievementIDs := dedupeTaskIDs(payload.GetIdList())
61+
if len(achievementIDs) == 0 {
62+
return client.SendMessage(21051, response)
63+
}
64+
65+
configsByID, err := loadIslandAchievementConfigByID()
66+
if err != nil {
67+
response.Result = proto.Uint32(islandAchievementClaimPersist)
68+
return client.SendMessage(21051, response)
69+
}
70+
71+
err = db.DefaultStore.WithPGXTx(context.Background(), func(tx pgx.Tx) error {
72+
state, err := orm.GetIslandAchievementStateForUpdateTx(context.Background(), tx, client.Commander.CommanderID)
73+
if err != nil {
74+
response.Result = proto.Uint32(islandAchievementClaimPersist)
75+
return err
76+
}
77+
78+
pendingDrops := make([]*protobuf.DROPINFO, 0)
79+
for _, achievementID := range achievementIDs {
80+
cfg, ok := configsByID[achievementID]
81+
if !ok {
82+
response.Result = proto.Uint32(islandAchievementClaimInvalid)
83+
return nil
84+
}
85+
if state.HasFinished(achievementID) {
86+
response.Result = proto.Uint32(islandAchievementClaimState)
87+
return nil
88+
}
89+
if !isIslandAchievementClaimEligible(state, cfg) {
90+
response.Result = proto.Uint32(islandAchievementClaimState)
91+
return nil
92+
}
93+
94+
awardRows, err := resolveIslandAchievementAwardRows(cfg)
95+
if err != nil {
96+
response.Result = proto.Uint32(islandAchievementClaimInvalid)
97+
return nil
98+
}
99+
drops, err := buildAwardDrops(awardRows)
100+
if err != nil {
101+
response.Result = proto.Uint32(islandAchievementClaimInvalid)
102+
return nil
103+
}
104+
pendingDrops = append(pendingDrops, drops...)
105+
state.FinishList = append(state.FinishList, achievementID)
106+
}
107+
108+
if err := applyIslandDropsTx(context.Background(), tx, client, pendingDrops); err != nil {
109+
response.Result = proto.Uint32(islandAchievementClaimPersist)
110+
return err
111+
}
112+
if err := orm.SaveIslandAchievementStateTx(context.Background(), tx, state); err != nil {
113+
response.Result = proto.Uint32(islandAchievementClaimPersist)
114+
return err
115+
}
116+
117+
response.Result = proto.Uint32(islandAchievementClaimOK)
118+
response.DropList = mergeDropList(pendingDrops)
119+
return nil
120+
})
121+
if err != nil {
122+
return client.SendMessage(21051, response)
123+
}
124+
125+
return client.SendMessage(21051, response)
126+
}
127+
128+
func IslandSyncAchievementProgress(buffer *[]byte, client *connection.Client) (int, int, error) {
129+
var payload protobuf.CS_21052
130+
if err := proto.Unmarshal(*buffer, &payload); err != nil {
131+
return 0, 21053, err
132+
}
133+
134+
response := &protobuf.SC_21053{EventList: []*protobuf.PB_ISLAND_ACHIEVENT{}}
135+
if err := ensureCommanderLoaded(client, "Island/AchievementSync"); err != nil {
136+
return 0, 21053, err
137+
}
138+
139+
rawEvents := payload.GetEventList()
140+
if len(rawEvents) > islandAchievementMaxSyncEvents {
141+
rawEvents = rawEvents[:islandAchievementMaxSyncEvents]
142+
}
143+
normalizedEvents := normalizeIslandAchievementEvents(rawEvents)
144+
configsByID, err := loadIslandAchievementConfigByID()
145+
if err != nil {
146+
return 0, 21053, err
147+
}
148+
syncRules := buildIslandAchievementSyncRules(configsByID)
149+
acceptedEvents := make([]orm.IslandAchievementProgressEntry, 0, len(normalizedEvents))
150+
151+
err = db.DefaultStore.WithPGXTx(context.Background(), func(tx pgx.Tx) error {
152+
state, err := orm.GetIslandAchievementStateForUpdateTx(context.Background(), tx, client.Commander.CommanderID)
153+
if err != nil {
154+
return err
155+
}
156+
for _, entry := range normalizedEvents {
157+
if !isIslandAchievementSyncEventAllowed(entry, syncRules) {
158+
continue
159+
}
160+
if current, exists := state.ProgressValue(entry.EventType, entry.EventArg); exists && entry.Value < current {
161+
continue
162+
}
163+
state.SetProgress(entry.EventType, entry.EventArg, entry.Value)
164+
acceptedEvents = append(acceptedEvents, entry)
165+
}
166+
if err := orm.SaveIslandAchievementStateTx(context.Background(), tx, state); err != nil {
167+
return err
168+
}
169+
response.EventList = buildIslandAchievementEventList(acceptedEvents)
170+
return nil
171+
})
172+
if err != nil {
173+
return 0, 21053, err
174+
}
175+
176+
return client.SendMessage(21053, response)
177+
}
178+
179+
func loadIslandAchievementConfigByID() (map[uint32]islandAchievementConfig, error) {
180+
entries, err := listConfigEntriesWithFallback(islandAchievementCategory, islandAchievementCategoryLC, orm.ListConfigEntries)
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
configs := make(map[uint32]islandAchievementConfig, len(entries))
186+
for _, entry := range entries {
187+
cfg := islandAchievementConfig{}
188+
if err := json.Unmarshal(entry.Data, &cfg); err != nil {
189+
return nil, err
190+
}
191+
if cfg.ID == 0 {
192+
parsedID, parseErr := strconv.ParseUint(entry.Key, 10, 32)
193+
if parseErr != nil {
194+
continue
195+
}
196+
cfg.ID = uint32(parsedID)
197+
}
198+
if cfg.ID == 0 {
199+
continue
200+
}
201+
configs[cfg.ID] = cfg
202+
}
203+
204+
return configs, nil
205+
}
206+
207+
func isIslandAchievementClaimEligible(state *orm.IslandAchievementState, cfg islandAchievementConfig) bool {
208+
if cfg.TargetType == 0 {
209+
return false
210+
}
211+
if cfg.TargetNum == 0 {
212+
return true
213+
}
214+
progress, ok := state.ProgressValue(cfg.TargetType, cfg.TargetValue)
215+
if !ok {
216+
return false
217+
}
218+
return progress >= cfg.TargetNum
219+
}
220+
221+
func resolveIslandAchievementAwardRows(cfg islandAchievementConfig) ([][]uint32, error) {
222+
if len(cfg.AwardDisplay) > 0 {
223+
return cfg.AwardDisplay, nil
224+
}
225+
if len(cfg.AwardRaw) == 0 {
226+
return nil, nil
227+
}
228+
229+
var matrix [][]uint32
230+
if err := json.Unmarshal(cfg.AwardRaw, &matrix); err == nil {
231+
return matrix, nil
232+
}
233+
234+
var single []uint32
235+
if err := json.Unmarshal(cfg.AwardRaw, &single); err == nil {
236+
if len(single) == 0 {
237+
return nil, nil
238+
}
239+
return [][]uint32{single}, nil
240+
}
241+
242+
return nil, json.Unmarshal(cfg.AwardRaw, &matrix)
243+
}
244+
245+
func normalizeIslandAchievementEvents(events []*protobuf.PB_ISLAND_ACHIEVENT) []orm.IslandAchievementProgressEntry {
246+
if len(events) == 0 {
247+
return []orm.IslandAchievementProgressEntry{}
248+
}
249+
250+
type eventKey struct {
251+
eventType uint32
252+
eventArg uint32
253+
}
254+
collapsed := make(map[eventKey]orm.IslandAchievementProgressEntry, len(events))
255+
for _, event := range events {
256+
if event == nil {
257+
continue
258+
}
259+
eventType := event.GetEventType()
260+
if eventType == 0 {
261+
continue
262+
}
263+
eventArg := event.GetEventArg()
264+
collapsed[eventKey{eventType: eventType, eventArg: eventArg}] = orm.IslandAchievementProgressEntry{
265+
EventType: eventType,
266+
EventArg: eventArg,
267+
Value: event.GetValue(),
268+
}
269+
}
270+
271+
normalized := make([]orm.IslandAchievementProgressEntry, 0, len(collapsed))
272+
for _, entry := range collapsed {
273+
normalized = append(normalized, entry)
274+
}
275+
sort.Slice(normalized, func(i, j int) bool {
276+
if normalized[i].EventType == normalized[j].EventType {
277+
return normalized[i].EventArg < normalized[j].EventArg
278+
}
279+
return normalized[i].EventType < normalized[j].EventType
280+
})
281+
return normalized
282+
}
283+
284+
func buildIslandAchievementSyncRules(configsByID map[uint32]islandAchievementConfig) map[islandAchievementEventKey]islandAchievementSyncRule {
285+
rules := make(map[islandAchievementEventKey]islandAchievementSyncRule, len(configsByID))
286+
for _, cfg := range configsByID {
287+
if cfg.TargetType == 0 || cfg.TargetNum == 0 {
288+
continue
289+
}
290+
key := islandAchievementEventKey{EventType: cfg.TargetType, EventArg: cfg.TargetValue}
291+
rule := rules[key]
292+
if cfg.TargetNum > rule.MaxValue {
293+
rule.MaxValue = cfg.TargetNum
294+
}
295+
rules[key] = rule
296+
}
297+
return rules
298+
}
299+
300+
func isIslandAchievementSyncEventAllowed(entry orm.IslandAchievementProgressEntry, rules map[islandAchievementEventKey]islandAchievementSyncRule) bool {
301+
rule, ok := rules[islandAchievementEventKey{EventType: entry.EventType, EventArg: entry.EventArg}]
302+
if !ok {
303+
return false
304+
}
305+
if rule.MaxValue == 0 {
306+
return false
307+
}
308+
return entry.Value <= rule.MaxValue
309+
}

0 commit comments

Comments
 (0)