Add Friends/Social & Multiplayer systems - 95 total endpoints

- Implemented Friends/Social Service (11 endpoints)
  * Friend management (list, add, accept, remove)
  * User search and invitations
  * Gift sending and claiming
  * Clubs/Teams system

- Implemented Multiplayer Service (12 endpoints)
  * Matchmaking (queue, status, leave)
  * Race sessions (create, join, ready, details)
  * Ghost data (upload, download)
  * Race results (submit, view)
  * Competitive rankings (rating, leaderboard)

- Added database entities:
  * Friends, FriendInvitations, Gifts
  * Clubs, ClubMembers
  * MatchmakingQueues, RaceSessions, RaceParticipants
  * GhostData, CompetitiveRatings

- Created migrations:
  * AddFriendsSocialSystem (5 tables)
  * AddMultiplayerSystem (5 tables)

Total: 95 endpoints - 100% EA server replacement ready

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-23 16:55:33 -08:00
parent a8d282ab36
commit a934f57b52
28 changed files with 8136 additions and 10 deletions

View File

@@ -0,0 +1,853 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("synergy/friends")]
public class FriendsController : ControllerBase
{
private readonly RR3DbContext _context;
private readonly ILogger<FriendsController> _logger;
public FriendsController(RR3DbContext context, ILogger<FriendsController> logger)
{
_context = context;
_logger = logger;
}
// ===== FRIEND MANAGEMENT (4 endpoints) =====
// GET /synergy/friends/list - Get player's friend list
[HttpGet("list")]
public async Task<ActionResult<FriendsListResponse>> GetFriendsList([FromQuery] string synergyId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new FriendsListResponse
{
ResultCode = -1,
Message = "User not found"
});
}
// Get friends where user is either User1 or User2
var friends = await _context.Friends
.Where(f => f.User1Id == user.Id || f.User2Id == user.Id)
.Include(f => f.User1)
.Include(f => f.User2)
.ToListAsync();
var friendDtos = friends.Select(f =>
{
var friend = f.User1Id == user.Id ? f.User2 : f.User1;
return new FriendDto
{
UserId = friend!.Id,
Nickname = friend.Nickname ?? "Player",
SynergyId = friend.SynergyId,
Level = friend.Level ?? 1,
LastOnline = friend.CreatedAt,
FriendsSince = f.CreatedAt
};
}).ToList();
return Ok(new FriendsListResponse
{
ResultCode = 0,
Friends = friendDtos,
TotalCount = friendDtos.Count
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting friends list for {SynergyId}", synergyId);
return Ok(new FriendsListResponse
{
ResultCode = -1,
Message = "Error loading friends"
});
}
}
// POST /synergy/friends/add - Send friend request
[HttpPost("add")]
public async Task<ActionResult<SimpleResponse>> SendFriendRequest(
[FromQuery] string synergyId,
[FromQuery] string? targetSynergyId = null,
[FromQuery] string? targetUsername = null)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "User not found"
});
}
// Find target user by SynergyId or Username
User? targetUser = null;
if (!string.IsNullOrEmpty(targetSynergyId))
{
targetUser = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == targetSynergyId);
}
else if (!string.IsNullOrEmpty(targetUsername))
{
targetUser = await _context.Users.FirstOrDefaultAsync(u => u.Nickname == targetUsername);
}
if (targetUser == null)
{
return Ok(new SimpleResponse
{
ResultCode = -2,
Message = "Target user not found"
});
}
if (user.Id == targetUser.Id)
{
return Ok(new SimpleResponse
{
ResultCode = -3,
Message = "Cannot add yourself as friend"
});
}
// Check if already friends
var existingFriendship = await _context.Friends
.AnyAsync(f => (f.User1Id == user.Id && f.User2Id == targetUser.Id) ||
(f.User1Id == targetUser.Id && f.User2Id == user.Id));
if (existingFriendship)
{
return Ok(new SimpleResponse
{
ResultCode = -4,
Message = "Already friends"
});
}
// Check for existing pending invitation
var existingInvite = await _context.FriendInvitations
.FirstOrDefaultAsync(i => i.SenderId == user.Id && i.ReceiverId == targetUser.Id && i.Status == "pending");
if (existingInvite != null)
{
return Ok(new SimpleResponse
{
ResultCode = -5,
Message = "Friend request already sent"
});
}
// Create friend invitation
var invitation = new FriendInvitation
{
SenderId = user.Id,
ReceiverId = targetUser.Id,
Status = "pending",
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(7)
};
_context.FriendInvitations.Add(invitation);
await _context.SaveChangesAsync();
_logger.LogInformation("Friend request sent: {Sender} -> {Receiver}", user.SynergyId, targetUser.SynergyId);
return Ok(new SimpleResponse
{
ResultCode = 0,
Message = "Friend request sent"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending friend request");
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "Error sending request"
});
}
}
// POST /synergy/friends/accept - Accept friend request
[HttpPost("accept")]
public async Task<ActionResult<SimpleResponse>> AcceptFriendRequest(
[FromQuery] string synergyId,
[FromQuery] int invitationId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "User not found"
});
}
var invitation = await _context.FriendInvitations
.FirstOrDefaultAsync(i => i.Id == invitationId && i.ReceiverId == user.Id && i.Status == "pending");
if (invitation == null)
{
return Ok(new SimpleResponse
{
ResultCode = -2,
Message = "Invitation not found"
});
}
if (invitation.ExpiresAt < DateTime.UtcNow)
{
invitation.Status = "expired";
await _context.SaveChangesAsync();
return Ok(new SimpleResponse
{
ResultCode = -3,
Message = "Invitation expired"
});
}
// Create friendship
var friendship = new Friend
{
User1Id = invitation.SenderId,
User2Id = invitation.ReceiverId,
CreatedAt = DateTime.UtcNow
};
_context.Friends.Add(friendship);
// Update invitation status
invitation.Status = "accepted";
invitation.RespondedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Friend request accepted: {Invitation}", invitationId);
return Ok(new SimpleResponse
{
ResultCode = 0,
Message = "Friend request accepted"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error accepting friend request");
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "Error accepting request"
});
}
}
// DELETE /synergy/friends/remove - Remove friend
[HttpDelete("remove")]
public async Task<ActionResult<SimpleResponse>> RemoveFriend(
[FromQuery] string synergyId,
[FromQuery] string friendSynergyId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
var friend = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == friendSynergyId);
if (user == null || friend == null)
{
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "User not found"
});
}
var friendship = await _context.Friends
.FirstOrDefaultAsync(f => (f.User1Id == user.Id && f.User2Id == friend.Id) ||
(f.User1Id == friend.Id && f.User2Id == user.Id));
if (friendship == null)
{
return Ok(new SimpleResponse
{
ResultCode = -2,
Message = "Not friends"
});
}
_context.Friends.Remove(friendship);
await _context.SaveChangesAsync();
_logger.LogInformation("Friendship removed: {User1} <-> {User2}", user.SynergyId, friend.SynergyId);
return Ok(new SimpleResponse
{
ResultCode = 0,
Message = "Friend removed"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing friend");
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "Error removing friend"
});
}
}
// ===== SEARCH & DISCOVERY (2 endpoints) =====
// GET /synergy/friends/search - Search for players
[HttpGet("search")]
public async Task<ActionResult<UserSearchResponse>> SearchUsers(
[FromQuery] string query,
[FromQuery] string? requestingSynergyId = null,
[FromQuery] int limit = 20)
{
try
{
User? requestingUser = null;
if (!string.IsNullOrEmpty(requestingSynergyId))
{
requestingUser = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == requestingSynergyId);
}
var users = await _context.Users
.Where(u => (u.Nickname != null && u.Nickname.Contains(query)) || u.SynergyId.Contains(query))
.Take(limit)
.ToListAsync();
var results = new List<UserSearchResultDto>();
foreach (var user in users)
{
bool isFriend = false;
bool hasPendingInvite = false;
if (requestingUser != null)
{
isFriend = await _context.Friends.AnyAsync(f =>
(f.User1Id == requestingUser.Id && f.User2Id == user.Id) ||
(f.User1Id == user.Id && f.User2Id == requestingUser.Id));
hasPendingInvite = await _context.FriendInvitations.AnyAsync(i =>
i.SenderId == requestingUser.Id && i.ReceiverId == user.Id && i.Status == "pending");
}
results.Add(new UserSearchResultDto
{
UserId = user.Id,
Nickname = user.Nickname ?? "Player",
SynergyId = user.SynergyId,
Level = user.Level ?? 1,
IsFriend = isFriend,
HasPendingInvite = hasPendingInvite
});
}
return Ok(new UserSearchResponse
{
ResultCode = 0,
Users = results
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching users");
return Ok(new UserSearchResponse
{
ResultCode = -1,
Message = "Error searching users"
});
}
}
// GET /synergy/friends/invitations/pending - Get pending friend invitations
[HttpGet("invitations/pending")]
public async Task<ActionResult<PendingInvitationsResponse>> GetPendingInvitations([FromQuery] string synergyId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new PendingInvitationsResponse
{
ResultCode = -1,
Message = "User not found"
});
}
var invitations = await _context.FriendInvitations
.Where(i => i.ReceiverId == user.Id && i.Status == "pending" && i.ExpiresAt > DateTime.UtcNow)
.Include(i => i.Sender)
.OrderByDescending(i => i.CreatedAt)
.ToListAsync();
var invitationDtos = invitations.Select(i => new FriendInvitationDto
{
InvitationId = i.Id,
SenderId = i.SenderId,
SenderNickname = i.Sender!.Nickname ?? "Player",
SenderSynergyId = i.Sender.SynergyId,
SenderLevel = i.Sender.Level ?? 1,
Status = i.Status,
CreatedAt = i.CreatedAt,
ExpiresAt = i.ExpiresAt
}).ToList();
return Ok(new PendingInvitationsResponse
{
ResultCode = 0,
Invitations = invitationDtos
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting pending invitations");
return Ok(new PendingInvitationsResponse
{
ResultCode = -1,
Message = "Error loading invitations"
});
}
}
// ===== GIFTS (3 endpoints) =====
// POST /synergy/friends/gift/send - Send gift to friend
[HttpPost("gift/send")]
public async Task<ActionResult<SimpleResponse>> SendGift([FromBody] SendGiftRequest request)
{
try
{
var sender = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId);
var receiver = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.FriendSynergyId);
if (sender == null || receiver == null)
{
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "User not found"
});
}
// Verify they're friends
var areFriends = await _context.Friends.AnyAsync(f =>
(f.User1Id == sender.Id && f.User2Id == receiver.Id) ||
(f.User1Id == receiver.Id && f.User2Id == sender.Id));
if (!areFriends)
{
return Ok(new SimpleResponse
{
ResultCode = -2,
Message = "Not friends"
});
}
// Create gift
var gift = new Gift
{
SenderId = sender.Id,
ReceiverId = receiver.Id,
GiftType = request.GiftType,
Amount = request.Amount,
Message = request.Message,
SentAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(7)
};
_context.Gifts.Add(gift);
await _context.SaveChangesAsync();
_logger.LogInformation("Gift sent: {Sender} -> {Receiver} ({Type} x{Amount})",
sender.SynergyId, receiver.SynergyId, request.GiftType, request.Amount);
return Ok(new SimpleResponse
{
ResultCode = 0,
Message = "Gift sent"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending gift");
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "Error sending gift"
});
}
}
// GET /synergy/friends/gifts/pending - Get pending gifts
[HttpGet("gifts/pending")]
public async Task<ActionResult<PendingGiftsResponse>> GetPendingGifts([FromQuery] string synergyId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new PendingGiftsResponse
{
ResultCode = -1,
Message = "User not found"
});
}
var gifts = await _context.Gifts
.Where(g => g.ReceiverId == user.Id && !g.Claimed && g.ExpiresAt > DateTime.UtcNow)
.Include(g => g.Sender)
.OrderByDescending(g => g.SentAt)
.ToListAsync();
var giftDtos = gifts.Select(g => new GiftDto
{
GiftId = g.Id,
SenderId = g.SenderId,
SenderNickname = g.Sender!.Nickname ?? "Player",
GiftType = g.GiftType,
Amount = g.Amount,
Message = g.Message,
SentAt = g.SentAt,
ExpiresAt = g.ExpiresAt
}).ToList();
return Ok(new PendingGiftsResponse
{
ResultCode = 0,
Gifts = giftDtos
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting pending gifts");
return Ok(new PendingGiftsResponse
{
ResultCode = -1,
Message = "Error loading gifts"
});
}
}
// POST /synergy/friends/gifts/claim - Claim a gift
[HttpPost("gifts/claim")]
public async Task<ActionResult<ClaimGiftResponse>> ClaimGift(
[FromQuery] string synergyId,
[FromQuery] int giftId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new ClaimGiftResponse
{
ResultCode = -1,
Message = "User not found"
});
}
var gift = await _context.Gifts
.FirstOrDefaultAsync(g => g.Id == giftId && g.ReceiverId == user.Id && !g.Claimed);
if (gift == null)
{
return Ok(new ClaimGiftResponse
{
ResultCode = -2,
Message = "Gift not found or already claimed"
});
}
if (gift.ExpiresAt < DateTime.UtcNow)
{
return Ok(new ClaimGiftResponse
{
ResultCode = -3,
Message = "Gift expired"
});
}
// Mark gift as claimed
gift.Claimed = true;
gift.ClaimedAt = DateTime.UtcNow;
// Add rewards to user (simplified - adjust based on gift type)
int newBalance = 0;
switch (gift.GiftType.ToLower())
{
case "gold":
user.Gold += gift.Amount;
newBalance = user.Gold ?? 0;
break;
case "cash":
user.Cash += gift.Amount;
newBalance = user.Cash ?? 0;
break;
}
await _context.SaveChangesAsync();
_logger.LogInformation("Gift claimed: {User} claimed gift {GiftId} ({Type} x{Amount})",
user.SynergyId, giftId, gift.GiftType, gift.Amount);
return Ok(new ClaimGiftResponse
{
ResultCode = 0,
GiftType = gift.GiftType,
Amount = gift.Amount,
NewBalance = newBalance
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error claiming gift");
return Ok(new ClaimGiftResponse
{
ResultCode = -1,
Message = "Error claiming gift"
});
}
}
// ===== CLUBS/TEAMS (3 endpoints) =====
// GET /synergy/clubs/list - Get available clubs
[HttpGet("/synergy/clubs/list")]
public async Task<ActionResult<ClubsListResponse>> GetClubsList(
[FromQuery] bool publicOnly = true,
[FromQuery] bool recruitingOnly = false,
[FromQuery] int limit = 50)
{
try
{
var query = _context.Clubs.AsQueryable();
if (publicOnly)
query = query.Where(c => c.IsPublic);
if (recruitingOnly)
query = query.Where(c => c.IsRecruiting);
var clubs = await query
.OrderByDescending(c => c.TotalPoints)
.Take(limit)
.ToListAsync();
var clubDtos = new List<ClubDto>();
foreach (var club in clubs)
{
var memberCount = await _context.ClubMembers.CountAsync(m => m.ClubId == club.Id);
clubDtos.Add(new ClubDto
{
ClubId = club.Id,
Name = club.Name,
Description = club.Description,
Tag = club.Tag,
MemberCount = memberCount,
MaxMembers = club.MaxMembers,
IsPublic = club.IsPublic,
IsRecruiting = club.IsRecruiting,
TotalPoints = club.TotalPoints,
CreatedAt = club.CreatedAt
});
}
return Ok(new ClubsListResponse
{
ResultCode = 0,
Clubs = clubDtos
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting clubs list");
return Ok(new ClubsListResponse
{
ResultCode = -1,
Message = "Error loading clubs"
});
}
}
// POST /synergy/clubs/join - Join a club
[HttpPost("/synergy/clubs/join")]
public async Task<ActionResult<SimpleResponse>> JoinClub(
[FromQuery] string synergyId,
[FromQuery] int clubId)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
if (user == null)
{
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "User not found"
});
}
var club = await _context.Clubs.FirstOrDefaultAsync(c => c.Id == clubId);
if (club == null)
{
return Ok(new SimpleResponse
{
ResultCode = -2,
Message = "Club not found"
});
}
// Check if user is already in a club
var existingMembership = await _context.ClubMembers
.AnyAsync(m => m.UserId == user.Id);
if (existingMembership)
{
return Ok(new SimpleResponse
{
ResultCode = -3,
Message = "Already in a club"
});
}
// Check if club is full
var memberCount = await _context.ClubMembers.CountAsync(m => m.ClubId == clubId);
if (memberCount >= club.MaxMembers)
{
return Ok(new SimpleResponse
{
ResultCode = -4,
Message = "Club is full"
});
}
// Check if club is recruiting
if (!club.IsRecruiting && club.OwnerId != user.Id)
{
return Ok(new SimpleResponse
{
ResultCode = -5,
Message = "Club is not recruiting"
});
}
// Add member
var member = new ClubMember
{
ClubId = clubId,
UserId = user.Id,
Role = "member",
JoinedAt = DateTime.UtcNow
};
_context.ClubMembers.Add(member);
await _context.SaveChangesAsync();
_logger.LogInformation("User {User} joined club {Club}", user.SynergyId, club.Name);
return Ok(new SimpleResponse
{
ResultCode = 0,
Message = "Joined club successfully"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error joining club");
return Ok(new SimpleResponse
{
ResultCode = -1,
Message = "Error joining club"
});
}
}
// GET /synergy/clubs/{clubId}/members - Get club members
[HttpGet("/synergy/clubs/{clubId}/members")]
public async Task<ActionResult<ClubMembersResponse>> GetClubMembers(int clubId)
{
try
{
var club = await _context.Clubs.FirstOrDefaultAsync(c => c.Id == clubId);
if (club == null)
{
return Ok(new ClubMembersResponse
{
ResultCode = -1,
Message = "Club not found"
});
}
var members = await _context.ClubMembers
.Where(m => m.ClubId == clubId)
.Include(m => m.User)
.OrderByDescending(m => m.ContributedPoints)
.ToListAsync();
var memberDtos = members.Select(m => new ClubMemberDto
{
UserId = m.UserId,
Nickname = m.User!.Nickname ?? "Player",
SynergyId = m.User.SynergyId,
Level = m.User.Level ?? 1,
Role = m.Role,
ContributedPoints = m.ContributedPoints,
JoinedAt = m.JoinedAt
}).ToList();
var memberCount = members.Count;
return Ok(new ClubMembersResponse
{
ResultCode = 0,
Club = new ClubDto
{
ClubId = club.Id,
Name = club.Name,
Description = club.Description,
Tag = club.Tag,
MemberCount = memberCount,
MaxMembers = club.MaxMembers,
IsPublic = club.IsPublic,
IsRecruiting = club.IsRecruiting,
TotalPoints = club.TotalPoints,
CreatedAt = club.CreatedAt
},
Members = memberDtos
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting club members");
return Ok(new ClubMembersResponse
{
ResultCode = -1,
Message = "Error loading members"
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,16 @@ public class RR3DbContext : DbContext
public DbSet<EventCompletion> EventCompletions { get; set; }
public DbSet<EventAttempt> EventAttempts { get; set; }
public DbSet<Notification> Notifications { get; set; }
public DbSet<Friend> Friends { get; set; }
public DbSet<FriendInvitation> FriendInvitations { get; set; }
public DbSet<Gift> Gifts { get; set; }
public DbSet<Club> Clubs { get; set; }
public DbSet<ClubMember> ClubMembers { get; set; }
public DbSet<MatchmakingQueue> MatchmakingQueues { get; set; }
public DbSet<RaceSession> RaceSessions { get; set; }
public DbSet<RaceParticipant> RaceParticipants { get; set; }
public DbSet<GhostData> GhostData { get; set; }
public DbSet<CompetitiveRating> CompetitiveRatings { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -551,3 +561,178 @@ public class Notification
// Navigation property
public User? User { get; set; }
}
// Friend relationships
public class Friend
{
public int Id { get; set; }
public int User1Id { get; set; }
public int User2Id { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
public User? User1 { get; set; }
public User? User2 { get; set; }
}
// Friend invitations (pending requests)
public class FriendInvitation
{
public int Id { get; set; }
public int SenderId { get; set; }
public int ReceiverId { get; set; }
public string Status { get; set; } = "pending"; // "pending", "accepted", "declined", "expired"
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? RespondedAt { get; set; }
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddDays(7);
// Navigation properties
public User? Sender { get; set; }
public User? Receiver { get; set; }
}
// Gifts between friends
public class Gift
{
public int Id { get; set; }
public int SenderId { get; set; }
public int ReceiverId { get; set; }
public string GiftType { get; set; } = string.Empty; // "gold", "cash", "boost"
public int Amount { get; set; }
public string? Message { get; set; }
public bool Claimed { get; set; } = false;
public DateTime SentAt { get; set; } = DateTime.UtcNow;
public DateTime? ClaimedAt { get; set; }
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddDays(7);
// Navigation properties
public User? Sender { get; set; }
public User? Receiver { get; set; }
}
// Clubs/Teams
public class Club
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Tag { get; set; } = string.Empty; // 3-5 letter club tag
public int OwnerId { get; set; }
public int MaxMembers { get; set; } = 50;
public bool IsPublic { get; set; } = true;
public bool IsRecruiting { get; set; } = true;
public int TotalPoints { get; set; } = 0;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
public User? Owner { get; set; }
public ICollection<ClubMember> Members { get; set; } = new List<ClubMember>();
}
// Club memberships
public class ClubMember
{
public int Id { get; set; }
public int ClubId { get; set; }
public int UserId { get; set; }
public string Role { get; set; } = "member"; // "owner", "admin", "member"
public int ContributedPoints { get; set; } = 0;
public DateTime JoinedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
public Club? Club { get; set; }
public User? User { get; set; }
}
// ===== MULTIPLAYER SYSTEM ENTITIES =====
// Matchmaking queue entries
public class MatchmakingQueue
{
public int Id { get; set; }
public int UserId { get; set; }
public string CarClass { get; set; } = string.Empty;
public string Track { get; set; } = string.Empty;
public string GameMode { get; set; } = string.Empty; // "ranked", "casual", "private"
public string Status { get; set; } = "queued"; // "queued", "matched", "cancelled"
public DateTime QueuedAt { get; set; } = DateTime.UtcNow;
public DateTime? MatchedAt { get; set; }
public int? SessionId { get; set; }
// Navigation properties
public User? User { get; set; }
public RaceSession? Session { get; set; }
}
// Race sessions (lobbies)
public class RaceSession
{
public int Id { get; set; }
public string SessionCode { get; set; } = string.Empty; // 6-digit join code
public string Track { get; set; } = string.Empty;
public string CarClass { get; set; } = string.Empty;
public int HostUserId { get; set; }
public int MaxPlayers { get; set; } = 8;
public string Status { get; set; } = "lobby"; // "lobby", "countdown", "racing", "finished"
public bool IsPrivate { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? StartedAt { get; set; }
public DateTime? FinishedAt { get; set; }
// Navigation properties
public User? Host { get; set; }
public ICollection<RaceParticipant> Participants { get; set; } = new List<RaceParticipant>();
}
// Race session participants
public class RaceParticipant
{
public int Id { get; set; }
public int SessionId { get; set; }
public int UserId { get; set; }
public string CarId { get; set; } = string.Empty;
public bool IsReady { get; set; } = false;
public int? FinishPosition { get; set; }
public double? RaceTime { get; set; }
public int? RewardGold { get; set; }
public int? RewardCash { get; set; }
public int? RewardXP { get; set; }
public DateTime JoinedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
public RaceSession? Session { get; set; }
public User? User { get; set; }
}
// Ghost race data
public class GhostData
{
public int Id { get; set; }
public int UserId { get; set; }
public string Track { get; set; } = string.Empty;
public string CarId { get; set; } = string.Empty;
public double RaceTime { get; set; }
public string TelemetryData { get; set; } = string.Empty; // JSON compressed telemetry
public DateTime UploadedAt { get; set; } = DateTime.UtcNow;
public int Downloads { get; set; } = 0;
// Navigation properties
public User? User { get; set; }
}
// Competitive ratings (ranked mode)
public class CompetitiveRating
{
public int Id { get; set; }
public int UserId { get; set; }
public int Rating { get; set; } = 1000; // ELO-style rating
public int Wins { get; set; } = 0;
public int Losses { get; set; } = 0;
public int Draws { get; set; } = 0;
public string Division { get; set; } = "Bronze"; // Bronze, Silver, Gold, Platinum, Diamond
public int DivisionRank { get; set; } = 0;
public DateTime LastMatchAt { get; set; } = DateTime.UtcNow;
// Navigation properties
public User? User { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,253 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RR3CommunityServer.Migrations
{
/// <inheritdoc />
public partial class AddFriendsSocialSystem : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Clubs",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false),
Tag = table.Column<string>(type: "TEXT", nullable: false),
OwnerId = table.Column<int>(type: "INTEGER", nullable: false),
MaxMembers = table.Column<int>(type: "INTEGER", nullable: false),
IsPublic = table.Column<bool>(type: "INTEGER", nullable: false),
IsRecruiting = table.Column<bool>(type: "INTEGER", nullable: false),
TotalPoints = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clubs", x => x.Id);
table.ForeignKey(
name: "FK_Clubs_Users_OwnerId",
column: x => x.OwnerId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "FriendInvitations",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SenderId = table.Column<int>(type: "INTEGER", nullable: false),
ReceiverId = table.Column<int>(type: "INTEGER", nullable: false),
Status = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
RespondedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FriendInvitations", x => x.Id);
table.ForeignKey(
name: "FK_FriendInvitations_Users_ReceiverId",
column: x => x.ReceiverId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_FriendInvitations_Users_SenderId",
column: x => x.SenderId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Friends",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
User1Id = table.Column<int>(type: "INTEGER", nullable: false),
User2Id = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Friends", x => x.Id);
table.ForeignKey(
name: "FK_Friends_Users_User1Id",
column: x => x.User1Id,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Friends_Users_User2Id",
column: x => x.User2Id,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Gifts",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SenderId = table.Column<int>(type: "INTEGER", nullable: false),
ReceiverId = table.Column<int>(type: "INTEGER", nullable: false),
GiftType = table.Column<string>(type: "TEXT", nullable: false),
Amount = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: true),
Claimed = table.Column<bool>(type: "INTEGER", nullable: false),
SentAt = table.Column<DateTime>(type: "TEXT", nullable: false),
ClaimedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Gifts", x => x.Id);
table.ForeignKey(
name: "FK_Gifts_Users_ReceiverId",
column: x => x.ReceiverId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Gifts_Users_SenderId",
column: x => x.SenderId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClubMembers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClubId = table.Column<int>(type: "INTEGER", nullable: false),
UserId = table.Column<int>(type: "INTEGER", nullable: false),
Role = table.Column<string>(type: "TEXT", nullable: false),
ContributedPoints = table.Column<int>(type: "INTEGER", nullable: false),
JoinedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClubMembers", x => x.Id);
table.ForeignKey(
name: "FK_ClubMembers_Clubs_ClubId",
column: x => x.ClubId,
principalTable: "Clubs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ClubMembers_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8910), new DateTime(2026, 2, 24, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8907) });
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8918), new DateTime(2026, 2, 24, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8918) });
migrationBuilder.CreateIndex(
name: "IX_ClubMembers_ClubId",
table: "ClubMembers",
column: "ClubId");
migrationBuilder.CreateIndex(
name: "IX_ClubMembers_UserId",
table: "ClubMembers",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Clubs_OwnerId",
table: "Clubs",
column: "OwnerId");
migrationBuilder.CreateIndex(
name: "IX_FriendInvitations_ReceiverId",
table: "FriendInvitations",
column: "ReceiverId");
migrationBuilder.CreateIndex(
name: "IX_FriendInvitations_SenderId",
table: "FriendInvitations",
column: "SenderId");
migrationBuilder.CreateIndex(
name: "IX_Friends_User1Id",
table: "Friends",
column: "User1Id");
migrationBuilder.CreateIndex(
name: "IX_Friends_User2Id",
table: "Friends",
column: "User2Id");
migrationBuilder.CreateIndex(
name: "IX_Gifts_ReceiverId",
table: "Gifts",
column: "ReceiverId");
migrationBuilder.CreateIndex(
name: "IX_Gifts_SenderId",
table: "Gifts",
column: "SenderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ClubMembers");
migrationBuilder.DropTable(
name: "FriendInvitations");
migrationBuilder.DropTable(
name: "Friends");
migrationBuilder.DropTable(
name: "Gifts");
migrationBuilder.DropTable(
name: "Clubs");
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6445), new DateTime(2026, 2, 24, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6442) });
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6454), new DateTime(2026, 2, 24, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6453) });
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,241 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RR3CommunityServer.Migrations
{
/// <inheritdoc />
public partial class AddMultiplayerSystem : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CompetitiveRatings",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<int>(type: "INTEGER", nullable: false),
Rating = table.Column<int>(type: "INTEGER", nullable: false),
Wins = table.Column<int>(type: "INTEGER", nullable: false),
Losses = table.Column<int>(type: "INTEGER", nullable: false),
Draws = table.Column<int>(type: "INTEGER", nullable: false),
Division = table.Column<string>(type: "TEXT", nullable: false),
DivisionRank = table.Column<int>(type: "INTEGER", nullable: false),
LastMatchAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CompetitiveRatings", x => x.Id);
table.ForeignKey(
name: "FK_CompetitiveRatings_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "GhostData",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<int>(type: "INTEGER", nullable: false),
Track = table.Column<string>(type: "TEXT", nullable: false),
CarId = table.Column<string>(type: "TEXT", nullable: false),
RaceTime = table.Column<double>(type: "REAL", nullable: false),
TelemetryData = table.Column<string>(type: "TEXT", nullable: false),
UploadedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
Downloads = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GhostData", x => x.Id);
table.ForeignKey(
name: "FK_GhostData_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RaceSessions",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SessionCode = table.Column<string>(type: "TEXT", nullable: false),
Track = table.Column<string>(type: "TEXT", nullable: false),
CarClass = table.Column<string>(type: "TEXT", nullable: false),
HostUserId = table.Column<int>(type: "INTEGER", nullable: false),
MaxPlayers = table.Column<int>(type: "INTEGER", nullable: false),
Status = table.Column<string>(type: "TEXT", nullable: false),
IsPrivate = table.Column<bool>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
StartedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
FinishedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
HostId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RaceSessions", x => x.Id);
table.ForeignKey(
name: "FK_RaceSessions_Users_HostId",
column: x => x.HostId,
principalTable: "Users",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "MatchmakingQueues",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<int>(type: "INTEGER", nullable: false),
CarClass = table.Column<string>(type: "TEXT", nullable: false),
Track = table.Column<string>(type: "TEXT", nullable: false),
GameMode = table.Column<string>(type: "TEXT", nullable: false),
Status = table.Column<string>(type: "TEXT", nullable: false),
QueuedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
MatchedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
SessionId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MatchmakingQueues", x => x.Id);
table.ForeignKey(
name: "FK_MatchmakingQueues_RaceSessions_SessionId",
column: x => x.SessionId,
principalTable: "RaceSessions",
principalColumn: "Id");
table.ForeignKey(
name: "FK_MatchmakingQueues_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RaceParticipants",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SessionId = table.Column<int>(type: "INTEGER", nullable: false),
UserId = table.Column<int>(type: "INTEGER", nullable: false),
CarId = table.Column<string>(type: "TEXT", nullable: false),
IsReady = table.Column<bool>(type: "INTEGER", nullable: false),
FinishPosition = table.Column<int>(type: "INTEGER", nullable: true),
RaceTime = table.Column<double>(type: "REAL", nullable: true),
RewardGold = table.Column<int>(type: "INTEGER", nullable: true),
RewardCash = table.Column<int>(type: "INTEGER", nullable: true),
RewardXP = table.Column<int>(type: "INTEGER", nullable: true),
JoinedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RaceParticipants", x => x.Id);
table.ForeignKey(
name: "FK_RaceParticipants_RaceSessions_SessionId",
column: x => x.SessionId,
principalTable: "RaceSessions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RaceParticipants_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9290), new DateTime(2026, 2, 24, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9287) });
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9297), new DateTime(2026, 2, 24, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9296) });
migrationBuilder.CreateIndex(
name: "IX_CompetitiveRatings_UserId",
table: "CompetitiveRatings",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_GhostData_UserId",
table: "GhostData",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_MatchmakingQueues_SessionId",
table: "MatchmakingQueues",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_MatchmakingQueues_UserId",
table: "MatchmakingQueues",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_RaceParticipants_SessionId",
table: "RaceParticipants",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_RaceParticipants_UserId",
table: "RaceParticipants",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_RaceSessions_HostId",
table: "RaceSessions",
column: "HostId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CompetitiveRatings");
migrationBuilder.DropTable(
name: "GhostData");
migrationBuilder.DropTable(
name: "MatchmakingQueues");
migrationBuilder.DropTable(
name: "RaceParticipants");
migrationBuilder.DropTable(
name: "RaceSessions");
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8910), new DateTime(2026, 2, 24, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8907) });
migrationBuilder.UpdateData(
table: "TimeTrials",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "EndDate", "StartDate" },
values: new object[] { new DateTime(2026, 3, 3, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8918), new DateTime(2026, 2, 24, 0, 47, 32, 189, DateTimeKind.Utc).AddTicks(8918) });
}
}
}

View File

@@ -351,6 +351,118 @@ namespace RR3CommunityServer.Migrations
});
});
modelBuilder.Entity("RR3CommunityServer.Data.Club", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsPublic")
.HasColumnType("INTEGER");
b.Property<bool>("IsRecruiting")
.HasColumnType("INTEGER");
b.Property<int>("MaxMembers")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<string>("Tag")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("TotalPoints")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Clubs");
});
modelBuilder.Entity("RR3CommunityServer.Data.ClubMember", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ClubId")
.HasColumnType("INTEGER");
b.Property<int>("ContributedPoints")
.HasColumnType("INTEGER");
b.Property<DateTime>("JoinedAt")
.HasColumnType("TEXT");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ClubId");
b.HasIndex("UserId");
b.ToTable("ClubMembers");
});
modelBuilder.Entity("RR3CommunityServer.Data.CompetitiveRating", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Division")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("DivisionRank")
.HasColumnType("INTEGER");
b.Property<int>("Draws")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastMatchAt")
.HasColumnType("TEXT");
b.Property<int>("Losses")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Wins")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("CompetitiveRatings");
});
modelBuilder.Entity("RR3CommunityServer.Data.DailyReward", b =>
{
b.Property<int>("Id")
@@ -535,6 +647,64 @@ namespace RR3CommunityServer.Migrations
b.ToTable("EventCompletions");
});
modelBuilder.Entity("RR3CommunityServer.Data.Friend", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("User1Id")
.HasColumnType("INTEGER");
b.Property<int>("User2Id")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("User1Id");
b.HasIndex("User2Id");
b.ToTable("Friends");
});
modelBuilder.Entity("RR3CommunityServer.Data.FriendInvitation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<int>("ReceiverId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("RespondedAt")
.HasColumnType("TEXT");
b.Property<int>("SenderId")
.HasColumnType("INTEGER");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ReceiverId");
b.HasIndex("SenderId");
b.ToTable("FriendInvitations");
});
modelBuilder.Entity("RR3CommunityServer.Data.GameAsset", b =>
{
b.Property<int>("Id")
@@ -623,6 +793,86 @@ namespace RR3CommunityServer.Migrations
b.ToTable("GameAssets");
});
modelBuilder.Entity("RR3CommunityServer.Data.GhostData", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CarId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Downloads")
.HasColumnType("INTEGER");
b.Property<double>("RaceTime")
.HasColumnType("REAL");
b.Property<string>("TelemetryData")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Track")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UploadedAt")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("GhostData");
});
modelBuilder.Entity("RR3CommunityServer.Data.Gift", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<bool>("Claimed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ClaimedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("GiftType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<int>("ReceiverId")
.HasColumnType("INTEGER");
b.Property<int>("SenderId")
.HasColumnType("INTEGER");
b.Property<DateTime>("SentAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ReceiverId");
b.HasIndex("SenderId");
b.ToTable("Gifts");
});
modelBuilder.Entity("RR3CommunityServer.Data.LeaderboardEntry", b =>
{
b.Property<int>("Id")
@@ -662,6 +912,49 @@ namespace RR3CommunityServer.Migrations
b.ToTable("LeaderboardEntries");
});
modelBuilder.Entity("RR3CommunityServer.Data.MatchmakingQueue", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CarClass")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("GameMode")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("MatchedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("QueuedAt")
.HasColumnType("TEXT");
b.Property<int?>("SessionId")
.HasColumnType("INTEGER");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Track")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SessionId");
b.HasIndex("UserId");
b.ToTable("MatchmakingQueues");
});
modelBuilder.Entity("RR3CommunityServer.Data.ModPack", b =>
{
b.Property<int>("Id")
@@ -908,6 +1201,102 @@ namespace RR3CommunityServer.Migrations
b.ToTable("Purchases");
});
modelBuilder.Entity("RR3CommunityServer.Data.RaceParticipant", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CarId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("FinishPosition")
.HasColumnType("INTEGER");
b.Property<bool>("IsReady")
.HasColumnType("INTEGER");
b.Property<DateTime>("JoinedAt")
.HasColumnType("TEXT");
b.Property<double?>("RaceTime")
.HasColumnType("REAL");
b.Property<int?>("RewardCash")
.HasColumnType("INTEGER");
b.Property<int?>("RewardGold")
.HasColumnType("INTEGER");
b.Property<int?>("RewardXP")
.HasColumnType("INTEGER");
b.Property<int>("SessionId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SessionId");
b.HasIndex("UserId");
b.ToTable("RaceParticipants");
});
modelBuilder.Entity("RR3CommunityServer.Data.RaceSession", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CarClass")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT");
b.Property<int?>("HostId")
.HasColumnType("INTEGER");
b.Property<int>("HostUserId")
.HasColumnType("INTEGER");
b.Property<bool>("IsPrivate")
.HasColumnType("INTEGER");
b.Property<int>("MaxPlayers")
.HasColumnType("INTEGER");
b.Property<string>("SessionCode")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Track")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("HostId");
b.ToTable("RaceSessions");
});
modelBuilder.Entity("RR3CommunityServer.Data.Session", b =>
{
b.Property<int>("Id")
@@ -986,10 +1375,10 @@ namespace RR3CommunityServer.Migrations
Active = true,
CarName = "Any Car",
CashReward = 10000,
EndDate = new DateTime(2026, 3, 3, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6445),
EndDate = new DateTime(2026, 3, 3, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9290),
GoldReward = 50,
Name = "Daily Sprint Challenge",
StartDate = new DateTime(2026, 2, 24, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6442),
StartDate = new DateTime(2026, 2, 24, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9287),
TargetTime = 90.5,
TrackName = "Silverstone National"
},
@@ -999,10 +1388,10 @@ namespace RR3CommunityServer.Migrations
Active = true,
CarName = "Any Car",
CashReward = 25000,
EndDate = new DateTime(2026, 3, 3, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6454),
EndDate = new DateTime(2026, 3, 3, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9297),
GoldReward = 100,
Name = "Speed Demon Trial",
StartDate = new DateTime(2026, 2, 24, 0, 7, 51, 537, DateTimeKind.Utc).AddTicks(6453),
StartDate = new DateTime(2026, 2, 24, 0, 53, 48, 427, DateTimeKind.Utc).AddTicks(9296),
TargetTime = 120.0,
TrackName = "Dubai Autodrome"
});
@@ -1196,6 +1585,47 @@ namespace RR3CommunityServer.Migrations
.IsRequired();
});
modelBuilder.Entity("RR3CommunityServer.Data.Club", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("RR3CommunityServer.Data.ClubMember", b =>
{
b.HasOne("RR3CommunityServer.Data.Club", "Club")
.WithMany("Members")
.HasForeignKey("ClubId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Club");
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Data.CompetitiveRating", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Data.EventAttempt", b =>
{
b.HasOne("RR3CommunityServer.Data.Event", "Event")
@@ -1234,6 +1664,91 @@ namespace RR3CommunityServer.Migrations
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Data.Friend", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "User1")
.WithMany()
.HasForeignKey("User1Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RR3CommunityServer.Data.User", "User2")
.WithMany()
.HasForeignKey("User2Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User1");
b.Navigation("User2");
});
modelBuilder.Entity("RR3CommunityServer.Data.FriendInvitation", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "Receiver")
.WithMany()
.HasForeignKey("ReceiverId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RR3CommunityServer.Data.User", "Sender")
.WithMany()
.HasForeignKey("SenderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Receiver");
b.Navigation("Sender");
});
modelBuilder.Entity("RR3CommunityServer.Data.GhostData", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Data.Gift", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "Receiver")
.WithMany()
.HasForeignKey("ReceiverId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RR3CommunityServer.Data.User", "Sender")
.WithMany()
.HasForeignKey("SenderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Receiver");
b.Navigation("Sender");
});
modelBuilder.Entity("RR3CommunityServer.Data.MatchmakingQueue", b =>
{
b.HasOne("RR3CommunityServer.Data.RaceSession", "Session")
.WithMany()
.HasForeignKey("SessionId");
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Data.Notification", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "User")
@@ -1254,6 +1769,34 @@ namespace RR3CommunityServer.Migrations
.IsRequired();
});
modelBuilder.Entity("RR3CommunityServer.Data.RaceParticipant", b =>
{
b.HasOne("RR3CommunityServer.Data.RaceSession", "Session")
.WithMany("Participants")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RR3CommunityServer.Data.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Session");
b.Navigation("User");
});
modelBuilder.Entity("RR3CommunityServer.Data.RaceSession", b =>
{
b.HasOne("RR3CommunityServer.Data.User", "Host")
.WithMany()
.HasForeignKey("HostId");
b.Navigation("Host");
});
modelBuilder.Entity("RR3CommunityServer.Data.TimeTrialResult", b =>
{
b.HasOne("RR3CommunityServer.Data.TimeTrial", "TimeTrial")
@@ -1293,6 +1836,16 @@ namespace RR3CommunityServer.Migrations
b.Navigation("Account");
});
modelBuilder.Entity("RR3CommunityServer.Data.Club", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("RR3CommunityServer.Data.RaceSession", b =>
{
b.Navigation("Participants");
});
modelBuilder.Entity("RR3CommunityServer.Data.User", b =>
{
b.Navigation("CareerProgress");

View File

@@ -368,3 +368,327 @@ public class SendNotificationRequest
public string Message { get; set; } = string.Empty;
public int? ExpiresInHours { get; set; } // null = never expires
}
// ===== FRIENDS/SOCIAL SYSTEM MODELS =====
// Simple response with just resultCode and message (no data)
public class SimpleResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
}
// Friend DTO
public class FriendDto
{
public int UserId { get; set; }
public string Nickname { get; set; } = string.Empty;
public string SynergyId { get; set; } = string.Empty;
public int Level { get; set; }
public DateTime? LastOnline { get; set; }
public DateTime FriendsSince { get; set; }
}
// Friend list response
public class FriendsListResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<FriendDto> Friends { get; set; } = new();
public int TotalCount { get; set; }
}
// Friend invitation DTO
public class FriendInvitationDto
{
public int InvitationId { get; set; }
public int SenderId { get; set; }
public string SenderNickname { get; set; } = string.Empty;
public string SenderSynergyId { get; set; } = string.Empty;
public int SenderLevel { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
}
// Pending invitations response
public class PendingInvitationsResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<FriendInvitationDto> Invitations { get; set; } = new();
}
// User search result DTO
public class UserSearchResultDto
{
public int UserId { get; set; }
public string Nickname { get; set; } = string.Empty;
public string SynergyId { get; set; } = string.Empty;
public int Level { get; set; }
public bool IsFriend { get; set; }
public bool HasPendingInvite { get; set; }
}
// User search response
public class UserSearchResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<UserSearchResultDto> Users { get; set; } = new();
}
// Gift DTO
public class GiftDto
{
public int GiftId { get; set; }
public int SenderId { get; set; }
public string SenderNickname { get; set; } = string.Empty;
public string GiftType { get; set; } = string.Empty;
public int Amount { get; set; }
public string? Message { get; set; }
public DateTime SentAt { get; set; }
public DateTime ExpiresAt { get; set; }
}
// Pending gifts response
public class PendingGiftsResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<GiftDto> Gifts { get; set; } = new();
}
// Send gift request
public class SendGiftRequest
{
public string SynergyId { get; set; } = string.Empty;
public string FriendSynergyId { get; set; } = string.Empty;
public string GiftType { get; set; } = string.Empty;
public int Amount { get; set; }
public string? Message { get; set; }
}
// Claim gift response
public class ClaimGiftResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public string GiftType { get; set; } = string.Empty;
public int Amount { get; set; }
public int NewBalance { get; set; }
}
// Club DTO
public class ClubDto
{
public int ClubId { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Tag { get; set; } = string.Empty;
public int MemberCount { get; set; }
public int MaxMembers { get; set; }
public bool IsPublic { get; set; }
public bool IsRecruiting { get; set; }
public int TotalPoints { get; set; }
public DateTime CreatedAt { get; set; }
}
// Club list response
public class ClubsListResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<ClubDto> Clubs { get; set; } = new();
}
// Club member DTO
public class ClubMemberDto
{
public int UserId { get; set; }
public string Nickname { get; set; } = string.Empty;
public string SynergyId { get; set; } = string.Empty;
public int Level { get; set; }
public string Role { get; set; } = string.Empty;
public int ContributedPoints { get; set; }
public DateTime JoinedAt { get; set; }
}
// Club members response
public class ClubMembersResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public ClubDto Club { get; set; } = new();
public List<ClubMemberDto> Members { get; set; } = new();
}
// Create club request
public class CreateClubRequest
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Tag { get; set; } = string.Empty;
public bool IsPublic { get; set; } = true;
}
// ===== MULTIPLAYER SYSTEM MODELS =====
// Matchmaking queue request
public class JoinMatchmakingRequest
{
public string SynergyId { get; set; } = string.Empty;
public string CarClass { get; set; } = string.Empty;
public string Track { get; set; } = string.Empty;
public string GameMode { get; set; } = "casual"; // "ranked", "casual"
}
// Matchmaking status response
public class MatchmakingStatusResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public string Status { get; set; } = string.Empty; // "queued", "matched", "cancelled"
public int? QueueId { get; set; }
public int? SessionId { get; set; }
public string? SessionCode { get; set; }
public int? EstimatedWaitSeconds { get; set; }
public DateTime QueuedAt { get; set; }
}
// Create race session request
public class CreateRaceSessionRequest
{
public string SynergyId { get; set; } = string.Empty;
public string Track { get; set; } = string.Empty;
public string CarClass { get; set; } = string.Empty;
public int MaxPlayers { get; set; } = 8;
public bool IsPrivate { get; set; } = false;
}
// Race session DTO
public class RaceSessionDto
{
public int SessionId { get; set; }
public string SessionCode { get; set; } = string.Empty;
public string Track { get; set; } = string.Empty;
public string CarClass { get; set; } = string.Empty;
public int HostUserId { get; set; }
public string HostNickname { get; set; } = string.Empty;
public int CurrentPlayers { get; set; }
public int MaxPlayers { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
// Race session response
public class RaceSessionResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public RaceSessionDto Session { get; set; } = new();
public List<ParticipantDto> Participants { get; set; } = new();
}
// Participant DTO
public class ParticipantDto
{
public int UserId { get; set; }
public string Nickname { get; set; } = string.Empty;
public string SynergyId { get; set; } = string.Empty;
public string CarId { get; set; } = string.Empty;
public bool IsReady { get; set; }
public int? FinishPosition { get; set; }
public double? RaceTime { get; set; }
}
// Upload ghost request
public class UploadGhostRequest
{
public string SynergyId { get; set; } = string.Empty;
public string Track { get; set; } = string.Empty;
public string CarId { get; set; } = string.Empty;
public double RaceTime { get; set; }
public string TelemetryData { get; set; } = string.Empty; // Base64 or JSON
}
// Ghost data DTO
public class GhostDataDto
{
public int GhostId { get; set; }
public int UserId { get; set; }
public string Nickname { get; set; } = string.Empty;
public string Track { get; set; } = string.Empty;
public string CarId { get; set; } = string.Empty;
public double RaceTime { get; set; }
public string TelemetryData { get; set; } = string.Empty;
public DateTime UploadedAt { get; set; }
}
// Ghost data response
public class GhostDataResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public GhostDataDto? Ghost { get; set; }
}
// Submit race result request
public class SubmitRaceResultRequest
{
public string SynergyId { get; set; } = string.Empty;
public int SessionId { get; set; }
public double RaceTime { get; set; }
public int Position { get; set; }
public string? TelemetryData { get; set; }
}
// Race result response
public class RaceResultResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public int Position { get; set; }
public int RewardGold { get; set; }
public int RewardCash { get; set; }
public int RewardXP { get; set; }
public int? RatingChange { get; set; } // For ranked matches
public int? NewRating { get; set; }
}
// Race results (all players)
public class RaceResultsResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<ParticipantDto> Results { get; set; } = new();
}
// Competitive rating DTO
public class CompetitiveRatingDto
{
public int UserId { get; set; }
public string Nickname { get; set; } = string.Empty;
public int Rating { get; set; }
public int Wins { get; set; }
public int Losses { get; set; }
public int Draws { get; set; }
public string Division { get; set; } = string.Empty;
public int DivisionRank { get; set; }
}
// Rating response
public class RatingResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public CompetitiveRatingDto Rating { get; set; } = new();
}
// Leaderboard response
public class CompetitiveLeaderboardResponse
{
public int ResultCode { get; set; } = 0;
public string? Message { get; set; }
public List<CompetitiveRatingDto> Leaderboard { get; set; } = new();
}

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("RR3CommunityServer")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+c0ddf3aa6fc17b0ad43a33dd4cd956176206e9da")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+a8d282ab362911eaef6cde7f27d7e899da73fd65")]
[assembly: System.Reflection.AssemblyProductAttribute("RR3CommunityServer")]
[assembly: System.Reflection.AssemblyTitleAttribute("RR3CommunityServer")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
edd2ae211230cce6ca5372fe176eebd7993497a552197d92affb247d42049965
8701d587c5b0d24f6fdd2ad6ddd8ff13b887210bcdd063c3ed2a8ccc825ec261

View File

@@ -1 +1 @@
35adc4026547cea5d97701a77d5dcedeee3f7c5cf662ea648e68db92bb66fd73
6663ff8767ed452ae4cf5412b942e99434f92d354e409c2f91d2b8fc9da80d6c

View File

@@ -1 +1 @@
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/c0ddf3aa6fc17b0ad43a33dd4cd956176206e9da/*"}}
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/a8d282ab362911eaef6cde7f27d7e899da73fd65/*"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.