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

Extra auctions hooks, update event leaderboard custom matching hook. #67

Merged
merged 5 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions achievements.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type AchievementsConfigAchievement struct {
Category string `json:"category,omitempty"`
Count int64 `json:"count,omitempty"`
Description string `json:"description,omitempty"`
StartTimeSec int64 `json:"start_time_sec,omitempty"`
EndTimeSec int64 `json:"end_time_sec,omitempty"`
ResetCronexpr string `json:"reset_cronexpr,omitempty"`
DurationSec int64 `json:"duration_sec,omitempty"`
MaxCount int64 `json:"max_count,omitempty"`
Expand Down
11 changes: 11 additions & 0 deletions auctions.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ type AuctionsConfigAuctionConditionFee struct {
Fixed *AuctionsConfigAuctionConditionBid `json:"fixed,omitempty"`
}

type OnAuctionReward[T any] func(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID, sourceID string, source *Auction, reward T) (T, error)

// The AuctionsSystem provides a gameplay system for Auctions and their listing, bidding, and timers.
//
// Players list items for auctioning, bid on other auctions, and collect their rewards when appropriate.
Expand Down Expand Up @@ -114,4 +116,13 @@ type AuctionsSystem interface {

// Follow ensures users receive real-time updates for auctions they have an interest in.
Follow(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID, sessionID string, auctionIDs []string) (*AuctionList, error)

// SetOnClaimBid sets a custom reward function which will run after an auction's reward is claimed by the winning bidder.
SetOnClaimBid(fn OnAuctionReward[*AuctionReward])

// SetOnClaimCreated sets a custom reward function which will run after an auction's winning bid is claimed by the auction creator.
SetOnClaimCreated(fn OnAuctionReward[*AuctionBidAmount])

// SetOnClaimCreatedFailed sets a custom reward function which will run after a failed auction is claimed by the auction creator.
SetOnClaimCreatedFailed(fn OnAuctionReward[*AuctionReward])
}
2 changes: 2 additions & 0 deletions base.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ type Hiro interface {
SetPersonalizer(Personalizer)
AddPersonalizer(personalizer Personalizer)

AddPublisher(publisher Publisher)

SetAfterAuthenticate(fn AfterAuthenticateFn)

// SetCollectionResolver sets a function that may change the storage collection target for Hiro systems. Not typically used.
Expand Down
9 changes: 8 additions & 1 deletion event_leaderboards.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,11 @@ type EventLeaderboardsSystem interface {
DebugRandomScores(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID, eventLeaderboardID string, scoreMin, scoreMax, subscoreMin, subscoreMax int64, operator *int) (eventLeaderboard *EventLeaderboard, err error)
}

type OnEventLeaderboardCohortSelection func(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, storageIndex string, eventID string, config *EventLeaderboardsConfigLeaderboard, userID string, tier int, matchmakerProperties map[string]interface{}) (cohortID string, cohortUserIDs []string, forceNewCohort bool, err error)
type EventLeaderboardCohortConfig struct {
// Force a new cohort even if cohort selection did not find an appropriate one.
ForceNewCohort bool `json:"force_new_cohort,omitempty"`
// Optionally use a specified tier instead of the expected one for the user.
Tier *int `json:"tier,omitempty"`
}

type OnEventLeaderboardCohortSelection func(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, storageIndex string, eventID string, config *EventLeaderboardsConfigLeaderboard, userID string, tier int, matchmakerProperties map[string]interface{}) (cohortID string, cohortUserIDs []string, newCohort *EventLeaderboardCohortConfig, err error)
1,811 changes: 917 additions & 894 deletions hiro.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions hiro.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2061,6 +2061,10 @@ message Achievement {
bool auto_claim_total = 20;
// Whether the achievement will reset after completion.
bool auto_reset = 21;
// The UNIX timestamp when this achievement will allow updates. This may be before its next reset. A zero means it is immediately available.
int64 start_time_sec = 22;
// The UNIX timestamp when this achievement will allow updates. This may be before its next reset. A zero means it does not end.
int64 end_time_sec = 23;
}

// The achievements returned by the server.
Expand Down
118 changes: 99 additions & 19 deletions personalizer_satori.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package hiro
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"sync/atomic"
Expand All @@ -25,25 +26,7 @@ import (
"github.com/heroiclabs/nakama-common/runtime"
)

type SatoriPublisher interface {
IsPublishAuthenticateRequest() bool
IsPublishAchievementsEvents() bool
IsPublishBaseEvents() bool
IsPublishEconomyEvents() bool
IsPublishEnergyEvents() bool
IsPublishEventLeaderboardsEvents() bool
IsPublishIncentivesEvents() bool
IsPublishInventoryEvents() bool
IsPublishLeaderboardsEvents() bool
IsPublishProgressionEvents() bool
IsPublishStatsEvents() bool
IsPublishTeamsEvents() bool
IsPublishTutorialsEvents() bool
IsPublishUnlockablesEvents() bool
IsPublishAuctionsEvents() bool
}

var _ SatoriPublisher = (*SatoriPersonalizer)(nil)
var _ Publisher = (*SatoriPersonalizer)(nil)

var _ Personalizer = (*SatoriPersonalizer)(nil)

Expand Down Expand Up @@ -235,6 +218,103 @@ type SatoriPersonalizer struct {
cache map[context.Context]*SatoriPersonalizerCache
}

func (p *SatoriPersonalizer) Authenticate(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID string, created bool) {
if !p.IsPublishAuthenticateRequest() {
return
}
if err := nk.GetSatori().Authenticate(ctx, userID); err != nil && !errors.Is(err, runtime.ErrSatoriConfigurationInvalid) {
logger.WithField("error", err.Error()).Error("failed to authenticate with Satori")
}
}

func (p *SatoriPersonalizer) Send(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID string, events []*PublisherEvent) {
if len(events) == 0 {
return
}

satoriEvents := make([]*runtime.Event, 0, len(events))
for _, event := range events {
switch event.System.GetType() {
case SystemTypeAchievements:
if !p.IsPublishAchievementsEvents() {
continue
}
case SystemTypeBase:
if !p.IsPublishBaseEvents() {
continue
}
case SystemTypeEconomy:
if !p.IsPublishEconomyEvents() {
continue
}
case SystemTypeEnergy:
if !p.IsPublishEnergyEvents() {
continue
}
case SystemTypeInventory:
if !p.IsPublishInventoryEvents() {
continue
}
case SystemTypeLeaderboards:
if !p.IsPublishLeaderboardsEvents() {
continue
}
case SystemTypeTeams:
if !p.IsPublishTeamsEvents() {
continue
}
case SystemTypeTutorials:
if !p.IsPublishTutorialsEvents() {
continue
}
case SystemTypeUnlockables:
if !p.IsPublishUnlockablesEvents() {
continue
}
case SystemTypeStats:
if !p.IsPublishStatsEvents() {
continue
}
case SystemTypeEventLeaderboards:
if !p.IsPublishEventLeaderboardsEvents() {
continue
}
case SystemTypeProgression:
if !p.IsPublishProgressionEvents() {
continue
}
case SystemTypeIncentives:
if !p.IsPublishIncentivesEvents() {
continue
}
case SystemTypeAuctions:
if !p.IsPublishAuctionsEvents() {
continue
}
case SystemTypeStreaks:
if !p.IsPublishStreaksEvents() {
continue
}
default:
}

satoriEvent := &runtime.Event{
Name: event.Name,
Id: event.Id,
Metadata: event.Metadata,
Value: event.Value,
Timestamp: event.Timestamp,
}
satoriEvents = append(satoriEvents, satoriEvent)
}
if len(satoriEvents) == 0 {
return
}
if err := nk.GetSatori().EventsPublish(ctx, userID, satoriEvents); err != nil {
logger.WithField("error", err.Error()).Error("failed to publish Satori events")
}
}

func NewSatoriPersonalizer(ctx context.Context, opts ...SatoriPersonalizerOption) *SatoriPersonalizer {
s := &SatoriPersonalizer{
cacheMutex: sync.RWMutex{},
Expand Down
21 changes: 15 additions & 6 deletions personalizer_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,20 @@ func rpcStoragePersonalizerUpload(initializer runtime.Initializer, p *StoragePer
return "", ErrSessionUser
}

decoder := json.NewDecoder(strings.NewReader(payload))
decoder.DisallowUnknownFields()

req := &storagePersonalizerUploadRequest{}
err := json.Unmarshal([]byte(payload), req)
if err != nil {

if err := decoder.Decode(req); err != nil {
logger.WithField("error", err.Error()).Error("decoder.Decode error")
if strings.HasPrefix(err.Error(), "json: unknown field") {
return "", runtime.NewError(err.Error(), 3)
}
return "", ErrPayloadDecode
}

writes := []*runtime.StorageWrite{}
writes := make([]*runtime.StorageWrite, 0, 15)

if req.Achievements != nil {
write, err := p.newStorageWrite(req.Achievements, storagePersonalizerKeyAchievements)
Expand Down Expand Up @@ -267,9 +274,11 @@ func rpcStoragePersonalizerUpload(initializer runtime.Initializer, p *StoragePer
writes = append(writes, write)
}

_, err = nk.StorageWrite(ctx, writes)
if err != nil {
return "", err
if len(writes) > 0 {
if _, err := nk.StorageWrite(ctx, writes); err != nil {
logger.WithField("error", err.Error()).Error("nk.StorageWrite error")
return "", err
}
}

return "{}", nil
Expand Down
55 changes: 55 additions & 0 deletions publisher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2024 Heroic Labs & Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hiro

import (
"context"

"github.com/heroiclabs/nakama-common/runtime"
)

type PublisherEvent struct {
Name string `json:"name,omitempty"`
Id string `json:"id,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Value string `json:"value,omitempty"`

// The Hiro system that generated this event.
System System `json:"-"`
// Source ID represents the identifier of the event source, such as an achievement ID.
SourceId string `json:"-"`
// Source represents the configuration of the event source, such as an achievement config.
Source any `json:"-"`
}

// The Publisher describes a service or similar target implementation that wishes to receive and process
// analytics-style events generated server-side by the various available Hiro systems.
//
// Each Publisher may choose to process or ignore each event as it sees fit. It may also choose to buffer
// events for batch processing at its discretion, but must take care to.
//
// Publisher implementations must safely handle concurrent calls.
//
// Implementations must handle any errors or retries internally, callers will not repeat calls in case
// of errors.
type Publisher interface {
// Authenticate is called every time a user authenticates with Hiro. The 'created' flag is true if this
// is a newly created user account, and each implementation may choose to handle this as it chooses.
Authenticate(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID string, created bool)

// Send is called when there are one or more events generated.
Send(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, userID string, events []*PublisherEvent)
}
Loading