Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"csharpier"
],
"rollForward": false
},
"dotnet-ef": {
"version": "9.0.8",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
}
45 changes: 44 additions & 1 deletion Turbo.Catalog/CatalogService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Turbo.Database.Context;
using Turbo.Primitives.Catalog;
using Turbo.Primitives.Catalog.Enums;
using Turbo.Primitives.Catalog.Providers;
Expand All @@ -10,12 +15,14 @@ namespace Turbo.Catalog;

public sealed class CatalogService(
ILogger<ICatalogService> logger,
ICatalogSnapshotProvider<NormalCatalog> normalCatalogProvider
ICatalogSnapshotProvider<NormalCatalog> normalCatalogProvider,
IDbContextFactory<TurboDbContext> dbCtxFactory
) : ICatalogService
{
private readonly ILogger<ICatalogService> _logger = logger;
private readonly ICatalogSnapshotProvider<NormalCatalog> _normalCatalogProvider =
normalCatalogProvider;
private readonly IDbContextFactory<TurboDbContext> _dbCtxFactory = dbCtxFactory;

public CatalogSnapshot GetCatalogSnapshot(CatalogType catalogType)
{
Expand All @@ -28,4 +35,40 @@ public CatalogSnapshot GetCatalogSnapshot(CatalogType catalogType)
_ => throw new NotSupportedException($"Catalog type {catalogType} is not supported."),
};
}

public async Task<UpcomingLtdSnapshot?> GetUpcomingLtdAsync(CancellationToken ct)
{
await using var dbCtx = await _dbCtxFactory.CreateDbContextAsync(ct);

// Find the nearest upcoming active LTD drop
var now = DateTime.UtcNow;
var nextSeries = await dbCtx
.LtdSeries.AsNoTracking()
.Where(s => s.IsActive && s.StartsAt > now)
.OrderBy(s => s.StartsAt)
.FirstOrDefaultAsync(ct);

if (nextSeries == null)
return null;

var catalogSnap = GetCatalogSnapshot(CatalogType.Normal);
var product = catalogSnap.ProductsById.Values.FirstOrDefault(p =>
p.LtdSeriesId == nextSeries.Id
);

if (product == null)
return null;

// Resolve PageId from Offer
if (!catalogSnap.OffersById.TryGetValue(product.OfferId, out var offer))
return null;

return new UpcomingLtdSnapshot
{
SecondsUntil = (int)(nextSeries.StartsAt!.Value - now).TotalSeconds,
PageId = offer.PageId,
OfferId = offer.Id,
ClassName = product.ClassName,
};
}
}
158 changes: 158 additions & 0 deletions Turbo.Catalog/Configuration/CatalogConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,164 @@
using System;

namespace Turbo.Catalog.Configuration;

public class CatalogConfig
{
public const string SECTION_NAME = "Turbo:Catalog";

/// <summary>
/// Configuration for LTD raffle weighting criteria.
/// </summary>
public LtdRaffleWeightConfig LtdRaffle { get; set; } = new();
}

/// <summary>
/// Configuration for LTD raffle weighting criteria.
/// Hotel owners can tune these values to define their own fairness criteria.
/// </summary>
public class LtdRaffleWeightConfig
{
/// <summary>
/// Base weight given to all participants (ensures everyone has a chance).
/// </summary>
public double BaseWeight { get; set; } = 1.0;

/// <summary>
/// Default buffer window in seconds.
/// </summary>
public int DefaultBufferSeconds { get; set; } = 20;

/// <summary>
/// If true, uses pure random selection (equal chance for all).
/// Set to true to disable all weighting.
/// </summary>
public bool UsePureRandom { get; set; } = false;

/// <summary>
/// If true, serial numbers are assigned randomly (Habbo style).
/// If false, they are assigned sequentially (1, 2, 3...).
/// </summary>
public bool RandomizeSerials { get; set; } = true;

/// <summary>
/// If true, each player can only win one item per LTD series.
/// If false, players can buy as many as they want (if stock permits).
/// </summary>
public bool LimitOnePerCustomer { get; set; } = true;

/// <summary>
/// Maximum number of entries accepted per raffle batch.
/// Prevents unbounded memory growth during the buffer window.
/// </summary>
public int MaxEntriesPerBatch { get; set; } = 5000;

/// <summary>
/// Badge count weighting configuration.
/// </summary>
public WeightCriterion BadgeCount { get; set; } =
new()
{
Enabled = true,
BonusPerUnit = 0.02,
MaxBonus = 1.0,
};

/// <summary>
/// Account age (days) weighting configuration.
/// </summary>
public WeightCriterion AccountAgeDays { get; set; } =
new()
{
Enabled = true,
BonusPerUnit = 0.00137, // ~0.5 per year
MaxBonus = 0.5,
};

/// <summary>
/// Online time (minutes) weighting configuration.
/// Note: Requires online time tracking to be implemented.
/// </summary>
public WeightCriterion OnlineTimeMinutes { get; set; } =
new()
{
Enabled = false,
BonusPerUnit = 0.00005,
MaxBonus = 0.5,
};

/// <summary>
/// Room count weighting configuration.
/// </summary>
public WeightCriterion RoomCount { get; set; } =
new()
{
Enabled = false,
BonusPerUnit = 0.05,
MaxBonus = 0.5,
};

/// <summary>
/// Furniture count weighting configuration.
/// </summary>
public WeightCriterion FurnitureCount { get; set; } =
new()
{
Enabled = false,
BonusPerUnit = 0.001,
MaxBonus = 0.5,
};

/// <summary>
/// Friend count weighting configuration.
/// </summary>
public WeightCriterion FriendCount { get; set; } =
new()
{
Enabled = false,
BonusPerUnit = 0.01,
MaxBonus = 0.5,
};

/// <summary>
/// Respects earned (from other users) weighting configuration.
/// </summary>
public WeightCriterion RespectsReceived { get; set; } =
new()
{
Enabled = false,
BonusPerUnit = 0.005,
MaxBonus = 0.5,
};

/// <summary>
/// Achievement score weighting configuration.
/// </summary>
public WeightCriterion AchievementScore { get; set; } =
new()
{
Enabled = false,
BonusPerUnit = 0.0001,
MaxBonus = 0.5,
};
}

/// <summary>
/// Configuration for a single weighting criterion.
/// </summary>
public class WeightCriterion
{
/// <summary>
/// Whether this criterion is used in weight calculation.
/// </summary>
public bool Enabled { get; set; }

/// <summary>
/// Bonus weight added per unit of this criterion.
/// </summary>
public double BonusPerUnit { get; set; }

/// <summary>
/// Maximum bonus that can be gained from this criterion.
/// </summary>
public double MaxBonus { get; set; }
}
Loading
Loading