Files
rr3-server/RR3CommunityServer/Controllers/RewardsController.cs
Daniel Elliott c0ddf3aa6f docs: Add EA legal compliance documentation
Documented EA's requirements for community servers:
- ⚖️ NO real money in-app purchases (PROHIBITED)
- ⚖️ NO charging for APK distribution (PROHIBITED)
-  Donations for server upkeep are allowed
-  All in-game content must be FREE

Added EA-LEGAL-AGREEMENT.md covering:
- Official EA policy (allowed vs prohibited)
- Server implementation requirements
- Donation guidelines and transparency
- Code examples for free economy
- Compliance checklist
- Disclaimers and legal notices

Updated RewardsController.cs:
- Added EA compliance comments to gold purchase endpoint
- Reinforced that Price MUST be 0
- Clear documentation that no real transactions allowed

This ensures the community server complies with EA's generous
allowance of community servers for this discontinued game.
2026-02-21 23:41:14 -08:00

295 lines
9.1 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Services;
using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("synergy/[controller]")]
public class RewardsController : ControllerBase
{
private readonly RR3DbContext _context;
private readonly ILogger<RewardsController> _logger;
public RewardsController(RR3DbContext context, ILogger<RewardsController> logger)
{
_context = context;
_logger = logger;
}
/// <summary>
/// Get daily reward status for a user
/// </summary>
[HttpGet("daily/{synergyId}")]
public async Task<IActionResult> GetDailyReward(string synergyId)
{
_logger.LogInformation("Getting daily reward for {SynergyId}", synergyId);
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return NotFound(new { error = "User not found" });
}
var today = DateTime.UtcNow.Date;
var reward = await _context.DailyRewards
.FirstOrDefaultAsync(r => r.UserId == user.Id && r.RewardDate.Date == today);
if (reward == null)
{
// Create new daily reward
reward = new DailyReward
{
UserId = user.Id,
RewardDate = DateTime.UtcNow,
GoldAmount = 50, // Daily gold reward
CashAmount = 5000, // Daily cash reward
Claimed = false,
Streak = await CalculateStreak(user.Id)
};
_context.DailyRewards.Add(reward);
await _context.SaveChangesAsync();
}
return Ok(new
{
available = !reward.Claimed,
gold = reward.GoldAmount,
cash = reward.CashAmount,
streak = reward.Streak,
nextRewardIn = reward.Claimed ? (24 - (DateTime.UtcNow - reward.RewardDate).TotalHours) : 0
});
}
/// <summary>
/// Claim daily reward
/// </summary>
[HttpPost("daily/{synergyId}/claim")]
public async Task<IActionResult> ClaimDailyReward(string synergyId)
{
_logger.LogInformation("Claiming daily reward for {SynergyId}", synergyId);
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return NotFound(new { error = "User not found" });
}
var today = DateTime.UtcNow.Date;
var reward = await _context.DailyRewards
.FirstOrDefaultAsync(r => r.UserId == user.Id && r.RewardDate.Date == today);
if (reward == null || reward.Claimed)
{
return BadRequest(new { error = "No reward available to claim" });
}
// Mark as claimed
reward.Claimed = true;
reward.ClaimedAt = DateTime.UtcNow;
// Update user's gold and cash
if (user.Gold == null) user.Gold = 0;
if (user.Cash == null) user.Cash = 0;
user.Gold += reward.GoldAmount;
user.Cash += reward.CashAmount;
await _context.SaveChangesAsync();
return Ok(new
{
success = true,
goldEarned = reward.GoldAmount,
cashEarned = reward.CashAmount,
totalGold = user.Gold,
totalCash = user.Cash,
streak = reward.Streak
});
}
/// <summary>
/// Purchase gold with real money (free in community server)
/// </summary>
/// <summary>
/// Purchase gold - FREE in community server per EA agreement
/// IMPORTANT: No real money transactions allowed!
/// </summary>
[HttpPost("gold/purchase")]
public async Task<IActionResult> PurchaseGold([FromBody] GoldPurchaseRequest request)
{
_logger.LogInformation("Processing gold purchase for {SynergyId}: {Amount} gold (FREE - community server)",
request.SynergyId, request.GoldAmount);
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId);
if (user == null)
{
return NotFound(new { error = "User not found" });
}
// ⚖️ EA COMPLIANCE: All gold purchases are FREE in community server!
// Per EA agreement: No real money in-app purchases allowed
if (user.Gold == null) user.Gold = 0;
user.Gold += request.GoldAmount;
// Log the purchase (for tracking, not billing)
var purchase = new Purchase
{
SynergyId = request.SynergyId,
UserId = user.Id,
ItemId = $"gold_{request.GoldAmount}",
Sku = request.Sku ?? $"com.ea.rr3.gold_{request.GoldAmount}",
OrderId = Guid.NewGuid().ToString(),
PurchaseTime = DateTime.UtcNow,
Token = Guid.NewGuid().ToString(),
Price = 0, // ⚖️ MUST BE 0 - EA COMPLIANCE
Status = "approved"
};
_context.Purchases.Add(purchase);
await _context.SaveChangesAsync();
return Ok(new
{
success = true,
goldPurchased = request.GoldAmount,
totalGold = user.Gold,
orderId = purchase.OrderId,
message = "Gold added to your account (FREE in community server!)"
});
}
/// <summary>
/// Get time trial events
/// </summary>
[HttpGet("timetrials")]
public async Task<IActionResult> GetTimeTrials()
{
_logger.LogInformation("Getting time trial events");
var trials = await _context.TimeTrials
.Where(t => t.Active)
.OrderBy(t => t.StartDate)
.ToListAsync();
return Ok(new
{
events = trials.Select(t => new
{
id = t.Id,
name = t.Name,
track = t.TrackName,
car = t.CarName,
startDate = t.StartDate,
endDate = t.EndDate,
goldReward = t.GoldReward,
cashReward = t.CashReward,
targetTime = t.TargetTime
})
});
}
/// <summary>
/// Submit time trial result
/// </summary>
[HttpPost("timetrials/{trialId}/submit")]
public async Task<IActionResult> SubmitTimeTrial(int trialId, [FromBody] TimeTrialSubmission submission)
{
_logger.LogInformation("Submitting time trial {TrialId} for {SynergyId}: {Time}s",
trialId, submission.SynergyId, submission.TimeSeconds);
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == submission.SynergyId);
if (user == null)
{
return NotFound(new { error = "User not found" });
}
var trial = await _context.TimeTrials.FindAsync(trialId);
if (trial == null || !trial.Active)
{
return NotFound(new { error = "Time trial not found or inactive" });
}
// Check if beat target time
bool beatTarget = submission.TimeSeconds <= trial.TargetTime;
int goldEarned = beatTarget ? trial.GoldReward : 0;
int cashEarned = beatTarget ? trial.CashReward : trial.CashReward / 2; // Half cash for participation
// Save result
var result = new TimeTrialResult
{
UserId = user.Id,
TimeTrialId = trialId,
TimeSeconds = submission.TimeSeconds,
SubmittedAt = DateTime.UtcNow,
BeatTarget = beatTarget,
GoldEarned = goldEarned,
CashEarned = cashEarned
};
_context.TimeTrialResults.Add(result);
// Award currency
if (user.Gold == null) user.Gold = 0;
if (user.Cash == null) user.Cash = 0;
user.Gold += goldEarned;
user.Cash += cashEarned;
await _context.SaveChangesAsync();
return Ok(new
{
success = true,
beatTarget = beatTarget,
timeSeconds = submission.TimeSeconds,
targetTime = trial.TargetTime,
goldEarned = goldEarned,
cashEarned = cashEarned,
totalGold = user.Gold,
totalCash = user.Cash,
message = beatTarget ? "🏆 Target time beaten!" : "Good try! Keep racing!"
});
}
private async Task<int> CalculateStreak(int userId)
{
var rewards = await _context.DailyRewards
.Where(r => r.UserId == userId && r.Claimed)
.OrderByDescending(r => r.RewardDate)
.ToListAsync();
int streak = 0;
var currentDate = DateTime.UtcNow.Date;
foreach (var reward in rewards)
{
if (reward.RewardDate.Date == currentDate.AddDays(-streak))
{
streak++;
}
else
{
break;
}
}
return streak;
}
}
public class GoldPurchaseRequest
{
public string SynergyId { get; set; } = string.Empty;
public int GoldAmount { get; set; }
public string? Sku { get; set; }
}
public class TimeTrialSubmission
{
public string SynergyId { get; set; } = string.Empty;
public double TimeSeconds { get; set; }
}