Skip to content

Commit

Permalink
Merge pull request #47 from emortalmc/feat/experience
Browse files Browse the repository at this point in the history
feat: implement experience
  • Loading branch information
ZakShearman authored Apr 7, 2024
2 parents de16d44 + f590d2a commit bbe3d19
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 34 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module mc-player-service
go 1.21

require (
github.com/emortalmc/proto-specs/gen/go v0.0.0-20231212225453-a8938507297c
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012921-6a9ad1aff227
github.com/google/uuid v1.5.0
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/segmentio/kafka-go v0.4.46
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20231212225453-a8938507297c h1:aXdLcj1nR3VAApCuHnOsSwzdIAZcMTm2LofIebfld5E=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20231212225453-a8938507297c/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240317132218-b310d52fd037 h1:t9Ct3hfgdKycSyMxytNFwFCpBYkEF5vHh1RLisBS0Yw=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240317132218-b310d52fd037/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240404195153-6dc4f4e385fc h1:qpIu8E1P/CkOZJ/XOcQnGWEISONjyn2fD4lj/F/ercY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240404195153-6dc4f4e385fc/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240405194818-f8231a77d2e5 h1:npqo8fIHM3jpHf73jtkTz9bz1EWn3NC+pAM0jMHiT0s=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240405194818-f8231a77d2e5/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406001747-c86b99d5483d h1:LJxka1f1NRbIi4pKa9yTaCUqMNtgb5wEBdsBiWX7CNM=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406001747-c86b99d5483d/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011652-11c567b404af h1:bctA2fXlQK9awuH6Oksav5fUwA9ESBfLrXNohM+gPrs=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011652-11c567b404af/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011759-782e66e8982a h1:9KbJ38ys1yri97anPeSJMiWsqJ+qWMUr2sGFCzdvlT8=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011759-782e66e8982a/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012812-1b77be1d5f2c h1:AUHtqD+LE49fFmtro7HpkcSy3gPv0StGrKmpTOXOCjQ=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012812-1b77be1d5f2c/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012921-6a9ad1aff227 h1:KXL6uPezjaPVPmlYE9UY4MzkSdqvo4L7gVTd/wQA96g=
github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012921-6a9ad1aff227/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down
11 changes: 7 additions & 4 deletions internal/app/mc_player.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
"mc-player-service/internal/app/player"
"mc-player-service/internal/config"
"mc-player-service/internal/grpc"
"mc-player-service/internal/kafka"
"mc-player-service/internal/kafka/consumer"
kafkaWriter "mc-player-service/internal/kafka/writer"
"mc-player-service/internal/repository"
"os/signal"
"sync"
Expand All @@ -33,12 +34,14 @@ func Run(cfg config.Config, log *zap.SugaredLogger) {
log.Fatalw("failed to create repository", err)
}

notifier := kafkaWriter.NewKafkaNotifier(ctx, wg, cfg.Kafka, log)

badgeSvc := badge.NewService(log, repo, repo, badgeCfg)
playerSvc := player.NewService(log, repo, cfg)
playerSvc := player.NewService(log, cfg, repo, notifier)

kafka.NewConsumer(ctx, wg, cfg, log, repo, badgeSvc, playerSvc)
kafkaConsumer.NewConsumer(ctx, wg, cfg, log, repo, badgeSvc, playerSvc)

grpc.RunServices(ctx, log, wg, cfg, badgeSvc, badgeCfg, repo)
grpc.RunServices(ctx, log, wg, cfg, badgeSvc, badgeCfg, playerSvc, repo)

wg.Wait()
log.Info("shutting down")
Expand Down
15 changes: 15 additions & 0 deletions internal/app/player/kafka_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package player

import (
"context"
"github.com/google/uuid"
kafkaWriter "mc-player-service/internal/kafka/writer"
)

var (
_ KafkaWriter = &kafkaWriter.Notifier{}
)

type KafkaWriter interface {
PlayerExperienceChange(ctx context.Context, playerID uuid.UUID, reason string, oldXP int, newXP int, oldLevel int, newLevel int)
}
41 changes: 36 additions & 5 deletions internal/app/player/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,64 @@ package player

import (
"context"
"fmt"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"mc-player-service/internal/config"
"mc-player-service/internal/repository"
"mc-player-service/internal/repository/model"
"mc-player-service/internal/utils/experience"
"mc-player-service/internal/webhook"
"time"
)

type Service interface {
HandlePlayerConnect(ctx context.Context, time time.Time, playerID uuid.UUID, playerUsername string,
proxyID string, playerSkin model.PlayerSkin, player model.Player)
proxyID string, playerSkin model.PlayerSkin, player model.Player)
HandlePlayerDisconnect(ctx context.Context, time time.Time, playerID uuid.UUID, playerUsername string)
HandlePlayerServerSwitch(ctx context.Context, pID uuid.UUID, newServerID string)

AddExperienceByID(ctx context.Context, playerID uuid.UUID, reason string, amount int) (int, error)
}

type serviceImpl struct {
log *zap.SugaredLogger

repo repository.PlayerReadWriter
repo repository.PlayerReadWriter
kafkaW KafkaWriter
webhook webhook.Webhook
}

func NewService(log *zap.SugaredLogger, repo repository.PlayerReadWriter, cfg config.Config) Service {
func NewService(log *zap.SugaredLogger, cfg config.Config, repo repository.PlayerReadWriter, kafkaW KafkaWriter) Service {
return &serviceImpl{
log: log,
repo: repo,
log: log,
repo: repo,
kafkaW: kafkaW,
webhook: webhook.NewWebhook(cfg.DiscordWebhookUrl, log),
}
}

func (s *serviceImpl) AddExperienceByID(ctx context.Context, playerID uuid.UUID, reason string, amount int) (int, error) {
newXP, err := s.repo.AddExperienceToPlayer(ctx, playerID, amount)
if err != nil {
return 0, fmt.Errorf("failed to add experience to player: %w", err)
}

if err := s.repo.CreateExperienceTransaction(ctx, model.ExperienceTransaction{
ID: primitive.NewObjectID(),
PlayerID: playerID,
Amount: int64(amount),
Reason: reason,
}); err != nil {
return 0, fmt.Errorf("failed to create experience transaction: %w", err)
}

oldXP := newXP - amount
oldLevel := experience.XPToLevel(oldXP)
newLevel := experience.XPToLevel(newXP)

s.kafkaW.PlayerExperienceChange(ctx, playerID, reason, oldXP, newXP, oldLevel, newLevel)

return newXP, nil
}
4 changes: 2 additions & 2 deletions internal/config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
)

type Config struct {
Kafka *KafkaConfig
MongoDB *MongoDBConfig
Kafka KafkaConfig
MongoDB MongoDBConfig

Development bool

Expand Down
37 changes: 36 additions & 1 deletion internal/grpc/mc_player.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"go.mongodb.org/mongo-driver/mongo"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"mc-player-service/internal/app/player"
"mc-player-service/internal/repository"
"mc-player-service/internal/repository/model"
"mc-player-service/internal/utils"
Expand All @@ -20,11 +21,13 @@ type mcPlayerService struct {
pb.McPlayerServer

repo repository.PlayerReader
svc player.Service
}

func newMcPlayerService(repo repository.PlayerReader) pb.McPlayerServer {
func newMcPlayerService(repo repository.PlayerReader, svc player.Service) pb.McPlayerServer {
return &mcPlayerService{
repo: repo,
svc: svc,
}
}

Expand Down Expand Up @@ -177,6 +180,38 @@ func (s *mcPlayerService) GetStatTotalPlaytime(ctx context.Context, _ *pb.GetSta
return &pb.GetStatTotalPlaytimeResponse{PlaytimeHours: count}, nil
}

func (s *mcPlayerService) AddExperienceToPlayers(ctx context.Context, req *pb.AddExperienceToPlayersRequest) (*pb.AddExperienceToPlayersResponse, error) {
ids := make([]uuid.UUID, len(req.PlayerIds))
for i, id := range req.PlayerIds {
pId, err := uuid.Parse(id)
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid player id %s", id))
}

ids[i] = pId
}

newXPs := make(map[string]uint64, len(ids))

for _, id := range ids {
newXP, err := s.svc.AddExperienceByID(ctx, id, req.Reason, int(req.Experience));

if err != nil {
return nil, fmt.Errorf("error adding experience to player %s: %w", id.String(), err)
}

newXPs[id.String()] = uint64(newXP)
}

return &pb.AddExperienceToPlayersResponse{
Experience: newXPs,
}, nil
}

func (s *mcPlayerService) GetPlayerExperience(ctx context.Context, req *pb.GetPlayerExperienceRequest) (*pb.GetPlayerExperienceResponse, error) {
panic("implement me")
}

func (s *mcPlayerService) getOrCreateMcPlayer(ctx context.Context, pId uuid.UUID) (*mcplayer.McPlayer, error) {
p, err := s.repo.GetPlayer(ctx, pId)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions internal/grpc/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
"mc-player-service/internal/app/badge"
"mc-player-service/internal/app/player"
"mc-player-service/internal/config"
"mc-player-service/internal/healthprovider"
"mc-player-service/internal/repository"
Expand All @@ -21,7 +22,7 @@ import (
)

func RunServices(ctx context.Context, log *zap.SugaredLogger, wg *sync.WaitGroup, cfg config.Config,
badgeSvc badge.Service, badgeCfg config.BadgeConfig, repo repository.Repository) {
badgeSvc badge.Service, badgeCfg config.BadgeConfig, playerSvc player.Service, repo repository.Repository) {

lis, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port))
if err != nil {
Expand All @@ -45,7 +46,7 @@ func RunServices(ctx context.Context, log *zap.SugaredLogger, wg *sync.WaitGroup
healthSrv := healthprovider.Create(ctx, repo)

grpc_health_v1.RegisterHealthServer(s, healthSrv)
mcplayer.RegisterMcPlayerServer(s, newMcPlayerService(repo))
mcplayer.RegisterMcPlayerServer(s, newMcPlayerService(repo, playerSvc))
badgeProto.RegisterBadgeManagerServer(s, newBadgeService(repo, badgeSvc, badgeCfg))
mcplayer.RegisterPlayerTrackerServer(s, newPlayerTrackerService(repo))
log.Infow("listening for gRPC requests", "port", cfg.Port)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kafka
package kafkaConsumer

import (
"context"
Expand Down
75 changes: 75 additions & 0 deletions internal/kafka/writer/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package kafkaWriter

import (
"context"
"fmt"
"github.com/emortalmc/proto-specs/gen/go/model/mcplayer"
"github.com/google/uuid"
"github.com/segmentio/kafka-go"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"mc-player-service/internal/config"
"sync"
"time"
)

const experienceWriterTopic = "player-experience"

type Notifier struct {
logger *zap.SugaredLogger
w *kafka.Writer
}

func NewKafkaNotifier(ctx context.Context, wg *sync.WaitGroup, cfg config.KafkaConfig, logger *zap.SugaredLogger) *Notifier {
w := &kafka.Writer{
Addr: kafka.TCP(cfg.Host),
Topic: experienceWriterTopic,
Balancer: &kafka.LeastBytes{},
Async: true,
BatchTimeout: 500 * time.Millisecond,
ErrorLogger: kafka.LoggerFunc(logger.Errorw),
}

wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
if err := w.Close(); err != nil {
logger.Errorw("failed to close kafka writer", "err", err)
}
}()

return &Notifier{
logger: logger,
w: w,
}
}

func (n *Notifier) PlayerExperienceChange(ctx context.Context, playerID uuid.UUID, reason string, oldXP int, newXP int, oldLevel int, newLevel int) {
msg := &mcplayer.PlayerExperienceChangeMessage{
PlayerId: playerID.String(),
Reason: reason,
PreviousExperience: int64(oldXP),
NewExperience: int64(newXP),
PreviousLevel: int32(oldLevel),
NewLevel: int32(newLevel),
}

if err := n.writeMessage(ctx, msg); err != nil {
n.logger.Errorw("failed to write message", "err", err)
return
}
}

func (n *Notifier) writeMessage(ctx context.Context, msg proto.Message) error {
bytes, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to marshal proto to bytes: %s", err)
}

return n.w.WriteMessages(ctx, kafka.Message{
Topic: experienceWriterTopic,
Headers: []kafka.Header{{Key: "X-Proto-Type", Value: []byte(msg.ProtoReflect().Descriptor().FullName())}},
Value: bytes,
})
}
10 changes: 10 additions & 0 deletions internal/repository/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Player struct {
ActiveBadge *string `bson:"activeBadge,omitempty"`

CurrentServer *CurrentServer `bson:"currentServer,omitempty"`

Experience int64 `bson:"experience,omitempty"`
}

func (p Player) IsEmpty() bool {
Expand All @@ -45,6 +47,7 @@ func (p Player) ToProto(session LoginSession) *mcplayer.McPlayer {
HistoricPlayTime: durationpb.New(p.TotalPlaytime),
CurrentServer: p.CurrentServer.ToProto(),
CurrentSkin: p.CurrentSkin.ToProto(),
Experience: uint64(p.Experience),
}
}

Expand Down Expand Up @@ -177,3 +180,10 @@ type PlayerUsername struct {
PlayerID uuid.UUID `bson:"playerId"`
Username string `bson:"username"`
}

type ExperienceTransaction struct {
ID primitive.ObjectID `bson:"_id"`
PlayerID uuid.UUID `bson:"playerId"`
Amount int64 `bson:"amount"`
Reason string `bson:"reason"`
}
Loading

0 comments on commit bbe3d19

Please sign in to comment.