--!strict
-- ============================================
-- DONATION MANAGER - SERVER SCRIPT
-- Gère DataStore, ProcessReceipt et Leaderboard
-- ============================================
-- EMPLACEMENT: ServerScriptService > DonationSystem > DonationManager
-- Services
local DataStoreService = game:GetService("DataStoreService")
local MarketplaceService = game:GetService("MarketplaceService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- DataStore
local DATASTORE_NAME = "DonationData_v1"
local donationDataStore = DataStoreService:GetDataStore(DATASTORE_NAME)
-- Configuration des produits (REMPLACEZ PAR VOS VRAIS PRODUCT IDs!)
local DONATION_PRODUCTS = {
[1234567890] = {Name = "STARTER", Amount = 10},
[1234567891] = {Name = "SUPPORTER", Amount = 50},
[1234567892] = {Name = "PREMIUM", Amount = 100},
[1234567893] = {Name = "ELITE", Amount = 250},
[1234567894] = {Name = "LEGENDARY", Amount = 500},
[1234567895] = {Name = "MYTHIC", Amount = 1000}
}
-- Cache du leaderboard (en mémoire pour performance)
local leaderboardCache: {{Username: string, UserId: number, TotalDonated: number, Rank: number}} = {}
local cacheLastUpdated = 0
-- Types
type PlayerData = {
Username: string,
TotalDonated: number,
LastUpdated: number
}
type DonatorData = {
Username: string,
UserId: number,
TotalDonated: number,
Rank: number
}
-- RemoteEvents/Functions
local donationSystemFolder = ReplicatedStorage:WaitForChild("DonationSystem")
local donationPurchasedEvent = donationSystemFolder:WaitForChild("DonationPurchased") :: RemoteEvent
local getLeaderboardFunction = donationSystemFolder:WaitForChild("GetLeaderboard") :: RemoteFunction
local getPlayerStatsFunction = donationSystemFolder:WaitForChild("GetPlayerStats") :: RemoteFunction
-- ============================================
-- DATASTORE FUNCTIONS
-- ============================================
local function getDataStoreKey(userId: number): string
return `Player_{userId}`
end
-- Charge les données d'un joueur
local function loadPlayerData(userId: number): PlayerData?
local key = getDataStoreKey(userId)
local success, result = pcall(function()
return donationDataStore:GetAsync(key)
end)
if success and result then
return result
elseif success then
-- Nouveau joueur, données par défaut
return {
Username = "Unknown",
TotalDonated = 0,
LastUpdated = os.time()
}
else
warn(`[DonationManager] Erreur lors du chargement des données pour UserId {userId}: {result}`)
return nil
end
end
-- Sauvegarde les données d'un joueur
local function savePlayerData(userId: number, data: PlayerData): boolean
local key = getDataStoreKey(userId)
local success, errorMsg = pcall(function()
donationDataStore:SetAsync(key, data)
end)
if not success then
warn(`[DonationManager] Erreur lors de la sauvegarde pour UserId {userId}: {errorMsg}`)
end
return success
end
-- ============================================
-- DONATION LOGIC
-- ============================================
-- Ajoute une donation au total du joueur
local function addDonation(userId: number, username: string, amount: number): boolean
print(`[DonationManager] Ajout donation: {username} ({userId}) - {amount} Robux`)
-- Charge les données actuelles
local playerData = loadPlayerData(userId)
if not playerData then
warn(`[DonationManager] Impossible de charger les données pour {userId}`)
return false
end
-- Met à jour les données
playerData.Username = username
playerData.TotalDonated = playerData.TotalDonated + amount
playerData.LastUpdated = os.time()
-- Sauvegarde
local success = savePlayerData(userId, playerData)
if success then
print(`[DonationManager] ✅ Donation enregistrée! Nouveau total: {playerData.TotalDonated} Robux`)
-- Met à jour le cache du leaderboard
updateLeaderboardCache()
-- Notifie tous les clients
donationPurchasedEvent:FireAllClients(userId, username, amount)
end
return success
end
-- ============================================
-- LEADERBOARD FUNCTIONS
-- ============================================
-- Met à jour le cache du leaderboard
function updateLeaderboardCache()
local allDonators: {DonatorData} = {}
-- Récupère tous les joueurs connectés + leurs données
for _, player in Players:GetPlayers() do
local data = loadPlayerData(player.UserId)
if data and data.TotalDonated > 0 then
table.insert(allDonators, {
Username = player.Name,
UserId = player.UserId,
TotalDonated = data.TotalDonated,
Rank = 0 -- Sera défini après le tri
})
end
end
-- Note: Dans un vrai système, vous voudriez aussi scanner le DataStore
-- pour les joueurs non connectés avec OrderedDataStore
-- Trie par TotalDonated (décroissant)
table.sort(allDonators, function(a, b)
return a.TotalDonated > b.TotalDonated
end)
-- Assigne les rangs
for rank, donator in allDonators do
donator.Rank = rank
end
leaderboardCache = allDonators
cacheLastUpdated = os.time()
print(`[DonationManager] Cache leaderboard mis à jour: {#allDonators} donateurs`)
end
-- Retourne le Top N donateurs
local function getTopDonators(limit: number): {DonatorData}
-- Rafraîchit le cache si plus vieux de 30 secondes
if os.time() - cacheLastUpdated > 30 then
updateLeaderboardCache()
end
local topDonators: {DonatorData} = {}
for i = 1, math.min(limit, #leaderboardCache) do
table.insert(topDonators, leaderboardCache[i])
end
return topDonators
end
-- Retourne les stats d'un joueur spécifique
local function getPlayerStats(userId: number): {Rank: number, TotalDonated: number}
local data = loadPlayerData(userId)
if not data then
return {Rank = 0, TotalDonated = 0}
end
-- Trouve le rang dans le cache
local rank = 0
for i, donator in leaderboardCache do
if donator.UserId == userId then
rank = i
break
end
end
return {
Rank = rank,
TotalDonated = data.TotalDonated
}
end
-- ============================================
-- MARKETPLACE PROCESSRECEIPT
-- ============================================
-- Callback automatique de Roblox lors d'un achat
local function processReceipt(receiptInfo)
local userId = receiptInfo.PlayerId
local productId = receiptInfo.ProductId
print(`[DonationManager] ProcessReceipt appelé: UserId={userId}, ProductId={productId}`)
-- Vérifie si c'est un produit de donation
local productConfig = DONATION_PRODUCTS[productId]
if not productConfig then
warn(`[DonationManager] ProductId {productId} non reconnu`)
return Enum.ProductPurchaseDecision.NotProcessedYet
end
-- Récupère le joueur
local player = Players:GetPlayerByUserId(userId)
if not player then
warn(`[DonationManager] Joueur {userId} non trouvé`)
return Enum.ProductPurchaseDecision.NotProcessedYet
end
-- Ajoute la donation
local success = addDonation(userId, player.Name, productConfig.Amount)
if success then
print(`[DonationManager] ✅ Achat traité: {player.Name} a donné {productConfig.Amount} Robux`)
return Enum.ProductPurchaseDecision.PurchaseGranted
else
warn(`[DonationManager] ❌ Échec du traitement de l'achat`)
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end
-- Enregistre le callback
MarketplaceService.ProcessReceipt = processReceipt
-- ============================================
-- REMOTE FUNCTION HANDLERS
-- ============================================
-- Gère les demandes de leaderboard des clients
getLeaderboardFunction.OnServerInvoke = function(player: Player, limit: number)
limit = limit or 10
return getTopDonators(limit)
end
-- Gère les demandes de stats des clients
getPlayerStatsFunction.OnServerInvoke = function(player: Player, userId: number?)
userId = userId or player.UserId
return getPlayerStats(userId)
end
-- ============================================
-- PLAYER EVENTS
-- ============================================
-- Sauvegarde finale quand un joueur quitte
Players.PlayerRemoving:Connect(function(player)
local data = loadPlayerData(player.UserId)
if data then
savePlayerData(player.UserId, data)
print(`[DonationManager] Données sauvegardées pour {player.Name}`)
end
end)
-- Met à jour le cache au démarrage
task.wait(2) -- Attend que les joueurs se connectent
updateLeaderboardCache()
-- Auto-refresh du cache toutes les 60 secondes
task.spawn(function()
while true do
task.wait(60)
updateLeaderboardCache()
end
end)
print("✅ [DonationManager] Système initialisé avec succès")
print(`📊 [DonationManager] {#DONATION_PRODUCTS} produits configurés`)
print(`💾 [DonationManager] DataStore: {DATASTORE_NAME}`)