New player events model
This commit is contained in:
parent
014862d642
commit
6d7ee2ed16
@ -39,15 +39,18 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat
|
|||||||
|> Array.map (fun item ->
|
|> Array.map (fun item ->
|
||||||
let action =
|
let action =
|
||||||
player.Events
|
player.Events
|
||||||
|> Array.tryFind (fun i -> i.ItemId = item.Id)
|
|> Array.tryFind (fun i ->
|
||||||
let btnColor = itemType
|
match i.Type with
|
||||||
|
| Hacking h -> h.HackId = item.Id
|
||||||
|
| Shielding id -> id = item.Id
|
||||||
|
| _ -> false)
|
||||||
|
let btnColor = Game.getClassButtonColor item.Class
|
||||||
match action , ignoreCooldown with
|
match action , ignoreCooldown with
|
||||||
| None , _ | Some _ , true ->
|
| None , _ | Some _ , true ->
|
||||||
DiscordButtonComponent(Game.getClassButtonColor item.Class, $"{actionId}-{item.Id}-{buttonInfo}-{player.Name}", $"{item.Name}")
|
DiscordButtonComponent(btnColor, $"{actionId}-{item.Id}-{buttonInfo}-{player.Name}", $"{item.Name}")
|
||||||
| Some act , false ->
|
| Some event , false ->
|
||||||
let c = ((Armory.getItem act.ItemId).Cooldown)
|
let time = Messaging.getShortTimeText (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp
|
||||||
let time = Messaging.getShortTimeText (TimeSpan.FromMinutes(int c)) act.Timestamp
|
DiscordButtonComponent(btnColor, $"{actionId}-{item.Id}", $"{item.Name} ({time} left)", true))
|
||||||
DiscordButtonComponent(Game.getClassButtonColor item.Class, $"{actionId}-{item.Id}", $"{item.Name} ({time} left)", true))
|
|
||||||
|> Seq.cast<DiscordComponent>
|
|> Seq.cast<DiscordComponent>
|
||||||
|
|
||||||
let pickDefense actionId player isTrainer =
|
let pickDefense actionId player isTrainer =
|
||||||
|
19
Bot/Game.fs
19
Bot/Game.fs
@ -76,10 +76,10 @@ module Player =
|
|||||||
let getShields (player : PlayerData) = getItems ItemType.Shield player
|
let getShields (player : PlayerData) = getItems ItemType.Shield player
|
||||||
let getHackEvents player =
|
let getHackEvents player =
|
||||||
player.Events
|
player.Events
|
||||||
|> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking -> true | _ -> false && act.ItemId < 12)
|
|> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking _ -> true | _ -> false)
|
||||||
let getShieldEvents player =
|
let getShieldEvents player =
|
||||||
player.Events
|
player.Events
|
||||||
|> Array.filter (fun act -> match act.Type with PlayerEventType.Shielding -> true | _ -> false && act.ItemId < 12)
|
|> Array.filter (fun act -> match act.Type with PlayerEventType.Shielding _ -> true | _ -> false)
|
||||||
|
|
||||||
let removeExpiredActions player =
|
let removeExpiredActions player =
|
||||||
let actions =
|
let actions =
|
||||||
@ -102,14 +102,17 @@ module Arsenal =
|
|||||||
| [||] -> "None"
|
| [||] -> "None"
|
||||||
| _ -> actions
|
| _ -> actions
|
||||||
|> Array.map (fun act ->
|
|> Array.map (fun act ->
|
||||||
let item = Armory.getItem act.ItemId
|
|
||||||
match act.Type with
|
match act.Type with
|
||||||
| PlayerEventType.Hacking ->
|
| Hacking h ->
|
||||||
|
let item = Armory.getItem h.HackId
|
||||||
let cooldown = Messaging.getTimeText false Game.SameTargetAttackCooldown act.Timestamp
|
let cooldown = Messaging.getTimeText false Game.SameTargetAttackCooldown act.Timestamp
|
||||||
$"Hacked {act.Adversary.Name} with {item.Name} {cooldown} ago"
|
$"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago"
|
||||||
| _ ->
|
| Shielding id ->
|
||||||
let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp
|
let item = Armory.getItem id
|
||||||
$"{item.Name} Shield active for {cooldown}")
|
let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int act.Cooldown)) act.Timestamp
|
||||||
|
$"{item.Name} Shield active for {cooldown}"
|
||||||
|
| _ -> "")
|
||||||
|
|> Array.filter (System.String.IsNullOrWhiteSpace)
|
||||||
|> String.concat "\n"
|
|> String.concat "\n"
|
||||||
|
|
||||||
let statusFormat p =
|
let statusFormat p =
|
||||||
|
@ -16,7 +16,10 @@ let checkPlayerIsAttackingThemselves defender attacker =
|
|||||||
|
|
||||||
let checkAlreadyHackedTarget defender attacker =
|
let checkAlreadyHackedTarget defender attacker =
|
||||||
defender.Events
|
defender.Events
|
||||||
|> Array.tryFind (fun event -> event.Adversary.Id = attacker.DiscordId && event.IsInstigator = false)
|
|> Array.tryFind (fun event ->
|
||||||
|
match event.Type with
|
||||||
|
| Hacking h -> h.Adversary.Id = attacker.DiscordId && h.IsInstigator = false
|
||||||
|
| _ -> false)
|
||||||
|> function
|
|> function
|
||||||
| Some event ->
|
| Some event ->
|
||||||
let cooldown = getTimeText true Game.SameTargetAttackCooldown event.Timestamp
|
let cooldown = getTimeText true Game.SameTargetAttackCooldown event.Timestamp
|
||||||
@ -24,9 +27,13 @@ let checkAlreadyHackedTarget defender attacker =
|
|||||||
| None -> Ok attacker
|
| None -> Ok attacker
|
||||||
|
|
||||||
let checkWeaponHasCooldown (weapon : Item) attacker =
|
let checkWeaponHasCooldown (weapon : Item) attacker =
|
||||||
let cooldown = attacker.Events |> Array.tryFind (fun a -> a.ItemId = weapon.Id)
|
attacker.Events
|
||||||
match cooldown with
|
|> Array.tryPick (fun a ->
|
||||||
| Some event ->
|
match a.Type with
|
||||||
|
| Hacking h -> if h.HackId = weapon.Id then Some ( h , a ) else None
|
||||||
|
| _ -> None)
|
||||||
|
|> function
|
||||||
|
| Some ( _ , event ) ->
|
||||||
let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp
|
let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp
|
||||||
Error $"{weapon.Name} is still active, it will expire in {cooldown}."
|
Error $"{weapon.Name} is still active, it will expire in {cooldown}."
|
||||||
| None -> Ok attacker
|
| None -> Ok attacker
|
||||||
@ -59,20 +66,21 @@ let calculateDamage (hack : Item) (shield : Item) =
|
|||||||
let runHackerBattle defender hack =
|
let runHackerBattle defender hack =
|
||||||
defender
|
defender
|
||||||
|> Player.removeExpiredActions
|
|> Player.removeExpiredActions
|
||||||
|> Player.getShieldEvents
|
|> fun p -> p.Events
|
||||||
|> Array.map (fun dfn -> Armory.battleItems |> Array.find (fun w -> w.Id = dfn.ItemId))
|
|> Array.choose (fun event ->
|
||||||
|> Array.map (calculateDamage (hack))
|
match event.Type with
|
||||||
|
| Shielding id -> Armory.battleItems |> Array.find (fun w -> w.Id = id) |> Some
|
||||||
|
| _ -> None)
|
||||||
|
|> Array.map (calculateDamage hack)
|
||||||
|> Array.contains Weak
|
|> Array.contains Weak
|
||||||
|
|
||||||
let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : Item) prize =
|
let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : Item) prize =
|
||||||
let updatePlayer amount attack p =
|
let updatePlayer amount attack p =
|
||||||
{ p with Events = Array.append [| attack |] p.Events ; Bank = max (p.Bank + amount) 0<GBT> }
|
{ p with Events = Array.append [| attack |] p.Events ; Bank = max (p.Bank + amount) 0<GBT> }
|
||||||
let event isDefenderEvent = {
|
let event isDefenderEvent =
|
||||||
ItemId = hack.Id
|
let hackEvent = {
|
||||||
Type = PlayerEventType.Hacking
|
HackId = hack.Id
|
||||||
Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer
|
Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer
|
||||||
Cooldown = if isDefenderEvent then Game.SameTargetAttackCooldown.Minutes * 1<mins> else hack.Cooldown
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
IsInstigator = not isDefenderEvent
|
IsInstigator = not isDefenderEvent
|
||||||
Result =
|
Result =
|
||||||
match successfulHack , isDefenderEvent with
|
match successfulHack , isDefenderEvent with
|
||||||
@ -81,6 +89,9 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa
|
|||||||
| true , false -> PlayerEventResult.Positive
|
| true , false -> PlayerEventResult.Positive
|
||||||
| false , false -> PlayerEventResult.Negative
|
| false , false -> PlayerEventResult.Negative
|
||||||
}
|
}
|
||||||
|
{ Type = Hacking hackEvent
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
Cooldown = if isDefenderEvent then Game.SameTargetAttackCooldown.Minutes * 1<mins> else hack.Cooldown }
|
||||||
|
|
||||||
[ DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer prize (event false) attacker
|
[ DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer prize (event false) attacker
|
||||||
DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender ]
|
DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender ]
|
||||||
@ -182,13 +193,9 @@ let handleDefense (ctx : IDiscordContext) =
|
|||||||
let embed = Embeds.responseCreatedShield shield
|
let embed = Embeds.responseCreatedShield shield
|
||||||
do! ctx.FollowUp embed |> Async.AwaitTask
|
do! ctx.FollowUp embed |> Async.AwaitTask
|
||||||
let defense = {
|
let defense = {
|
||||||
ItemId = shieldId
|
Type = Shielding shieldId
|
||||||
Type = PlayerEventType.Shielding
|
|
||||||
Result = PlayerEventResult.Positive
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
Cooldown = shield.Cooldown
|
Cooldown = shield.Cooldown
|
||||||
IsInstigator = true
|
Timestamp = DateTime.UtcNow
|
||||||
Adversary = DiscordPlayer.empty
|
|
||||||
}
|
}
|
||||||
do! DbService.updatePlayer GuildEnvironment.pgDb <| { p with Events = Array.append [| defense |] p.Events }
|
do! DbService.updatePlayer GuildEnvironment.pgDb <| { p with Events = Array.append [| defense |] p.Events }
|
||||||
|> Async.Ignore
|
|> Async.Ignore
|
||||||
|
@ -68,10 +68,7 @@ let handleSell (ctx : IDiscordContext) itemId =
|
|||||||
player with
|
player with
|
||||||
Bank = player.Bank + item.Price
|
Bank = player.Bank + item.Price
|
||||||
Inventory = player.Inventory |> Array.filter (fun i -> i.Id <> itemId)
|
Inventory = player.Inventory |> Array.filter (fun i -> i.Id <> itemId)
|
||||||
Events =
|
Events = player.Events |> Array.filter (fun e -> match e.Type with Shielding _ -> true | _ -> false)
|
||||||
if item.Type = ItemType.Shield
|
|
||||||
then player.Events |> Array.filter (fun a -> a.ItemId <> itemId)
|
|
||||||
else player.Events
|
|
||||||
}
|
}
|
||||||
do! DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer
|
do! DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer
|
||||||
|> Async.Ignore
|
|> Async.Ignore
|
||||||
|
35
Bot/Thief.fs
35
Bot/Thief.fs
@ -66,11 +66,13 @@ let getResultEmbed chance prize (bank : int<GBT>) thief (victim : DiscordPlayer)
|
|||||||
.WithDescription(msg)
|
.WithDescription(msg)
|
||||||
.WithImageUrl(img)
|
.WithImageUrl(img)
|
||||||
|
|
||||||
|
// TODO: See if we are going to keep this
|
||||||
let checkVictimStealingCooldown defender attacker =
|
let checkVictimStealingCooldown defender attacker =
|
||||||
defender
|
defender
|
||||||
|> Player.removeExpiredActions
|
|> Player.removeExpiredActions
|
||||||
|> fun p -> p.Events
|
|> fun p -> p.Events
|
||||||
|> Array.tryFind (fun pe -> pe.Type = PlayerEventType.Steal && pe.Result = PlayerEventResult.Negative)
|
|> Array.tryFind (fun e ->
|
||||||
|
match e.Type with Stealing _ -> true | _ -> false)
|
||||||
|> function
|
|> function
|
||||||
| Some act ->
|
| Some act ->
|
||||||
let cooldown = VictimRecovery - (DateTime.UtcNow - act.Timestamp)
|
let cooldown = VictimRecovery - (DateTime.UtcNow - act.Timestamp)
|
||||||
@ -82,7 +84,7 @@ let checkThiefCooldown attacker =
|
|||||||
attacker
|
attacker
|
||||||
|> Player.removeExpiredActions
|
|> Player.removeExpiredActions
|
||||||
|> fun p -> p.Events
|
|> fun p -> p.Events
|
||||||
|> Array.tryFind (fun pe -> pe.Type = PlayerEventType.Steal)
|
|> Array.tryFind (fun pe -> match pe.Type with Stealing _ -> true | _ -> false)
|
||||||
|> function
|
|> function
|
||||||
| Some act ->
|
| Some act ->
|
||||||
let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp)
|
let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp)
|
||||||
@ -104,8 +106,9 @@ let calculateWinPercentage amountRequested bank attackerStrength defenderStrengt
|
|||||||
|
|
||||||
let steal target amount (ctx : IDiscordContext) =
|
let steal target amount (ctx : IDiscordContext) =
|
||||||
Game.executePlayerActionWithTarget target ctx (fun thief victim -> async { do!
|
Game.executePlayerActionWithTarget target ctx (fun thief victim -> async { do!
|
||||||
thief
|
// thief
|
||||||
|> checkVictimStealingCooldown victim
|
// |> checkVictimStealingCooldown victim
|
||||||
|
Ok thief
|
||||||
|> handleResultWithResponse ctx (fun _ -> async {
|
|> handleResultWithResponse ctx (fun _ -> async {
|
||||||
let cappedPrize , winPercentage , wasCapped =
|
let cappedPrize , winPercentage , wasCapped =
|
||||||
calculateWinPercentage amount (int victim.Bank) thief.Traits.Strength victim.Traits.Strength
|
calculateWinPercentage amount (int victim.Bank) thief.Traits.Strength victim.Traits.Strength
|
||||||
@ -149,11 +152,7 @@ let handleSteal (ctx : IDiscordContext) =
|
|||||||
let num = rand.NextDouble()
|
let num = rand.NextDouble()
|
||||||
let dp = { DiscordPlayer.Id = targetId ; DiscordPlayer.Name = targetName }
|
let dp = { DiscordPlayer.Id = targetId ; DiscordPlayer.Name = targetName }
|
||||||
let stealAction result = {
|
let stealAction result = {
|
||||||
ItemId = -1
|
Type = Stealing ( true , dp )
|
||||||
Type = PlayerEventType.Steal
|
|
||||||
Result = result
|
|
||||||
Adversary = dp
|
|
||||||
IsInstigator = true
|
|
||||||
Cooldown = ThiefCooldown.Minutes * 1<mins>
|
Cooldown = ThiefCooldown.Minutes * 1<mins>
|
||||||
Timestamp = DateTime.UtcNow
|
Timestamp = DateTime.UtcNow
|
||||||
}
|
}
|
||||||
@ -167,12 +166,8 @@ let handleSteal (ctx : IDiscordContext) =
|
|||||||
match! DbService.tryFindPlayer GuildEnvironment.pgDb targetId with
|
match! DbService.tryFindPlayer GuildEnvironment.pgDb targetId with
|
||||||
| Some t ->
|
| Some t ->
|
||||||
let mugged = {
|
let mugged = {
|
||||||
ItemId = -1
|
Type = Stealing ( false , thief.basicPlayer )
|
||||||
Type = PlayerEventType.Steal
|
|
||||||
Result = PlayerEventResult.Negative
|
|
||||||
Adversary = thief.basicPlayer
|
|
||||||
Timestamp = DateTime.UtcNow
|
Timestamp = DateTime.UtcNow
|
||||||
IsInstigator = false
|
|
||||||
Cooldown = VictimRecovery.Minutes * 1<mins>
|
Cooldown = VictimRecovery.Minutes * 1<mins>
|
||||||
}
|
}
|
||||||
let actions = t |> Player.removeExpiredActions |> fun p -> Array.append [| mugged |] p.Events
|
let actions = t |> Player.removeExpiredActions |> fun p -> Array.append [| mugged |] p.Events
|
||||||
@ -181,13 +176,9 @@ let handleSteal (ctx : IDiscordContext) =
|
|||||||
| None -> ()
|
| None -> ()
|
||||||
|
|
||||||
let stole = {
|
let stole = {
|
||||||
ItemId = -1
|
Type = Stealing ( true , dp )
|
||||||
Type = PlayerEventType.Steal
|
|
||||||
Result = PlayerEventResult.Positive
|
|
||||||
Adversary = dp
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
IsInstigator = true
|
|
||||||
Cooldown = ThiefCooldown.Minutes * 1<mins>
|
Cooldown = ThiefCooldown.Minutes * 1<mins>
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
}
|
}
|
||||||
let actions = thief |> Player.removeExpiredActions |> fun p -> Array.append [| stole |] p.Events
|
let actions = thief |> Player.removeExpiredActions |> fun p -> Array.append [| stole |] p.Events
|
||||||
do! DbService.updatePlayer GuildEnvironment.pgDb { thief with Bank = thief.Bank + prize ; Events = actions }
|
do! DbService.updatePlayer GuildEnvironment.pgDb { thief with Bank = thief.Bank + prize ; Events = actions }
|
||||||
@ -206,8 +197,8 @@ let handleSteal (ctx : IDiscordContext) =
|
|||||||
Game.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async {
|
Game.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async {
|
||||||
do! attacker
|
do! attacker
|
||||||
|> Player.removeExpiredActions
|
|> Player.removeExpiredActions
|
||||||
|> checkVictimStealingCooldown defender
|
// |> checkVictimStealingCooldown defender
|
||||||
>>= checkThiefCooldown
|
|> checkThiefCooldown
|
||||||
|> handleResultWithResponse ctx (handleYes defender )
|
|> handleResultWithResponse ctx (handleYes defender )
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
|
@ -15,19 +15,15 @@ let defaultShield = Armory.battleItems |> Array.find (fun i -> i.Id = int Shield
|
|||||||
|
|
||||||
let TrainerEvents = [|
|
let TrainerEvents = [|
|
||||||
{ Timestamp = System.DateTime.UtcNow
|
{ Timestamp = System.DateTime.UtcNow
|
||||||
|
Cooldown = 2<mins>
|
||||||
|
Type = Hacking {
|
||||||
Adversary = Sensei
|
Adversary = Sensei
|
||||||
Type = PlayerEventType.Hacking
|
|
||||||
Result = PlayerEventResult.Positive
|
Result = PlayerEventResult.Positive
|
||||||
Cooldown = 5<mins>
|
|
||||||
IsInstigator = true
|
IsInstigator = true
|
||||||
ItemId = defaultHack.Id }
|
HackId = defaultHack.Id } }
|
||||||
{ Timestamp = System.DateTime.UtcNow
|
{ Timestamp = System.DateTime.UtcNow
|
||||||
Adversary = DiscordPlayer.empty
|
|
||||||
Type = PlayerEventType.Shielding
|
|
||||||
Result = PlayerEventResult.Positive
|
|
||||||
Cooldown = defaultShield.Cooldown
|
Cooldown = defaultShield.Cooldown
|
||||||
IsInstigator = true
|
Type = Shielding defaultShield.Id }
|
||||||
ItemId = defaultShield.Id }
|
|
||||||
|]
|
|]
|
||||||
|
|
||||||
let sendInitialEmbed (client : DiscordClient) =
|
let sendInitialEmbed (client : DiscordClient) =
|
||||||
|
@ -73,8 +73,8 @@ let updatePlayer connStr (player : PlayerData) =
|
|||||||
let addAchievement connStr (did : uint64) (achievement : string) =
|
let addAchievement connStr (did : uint64) (achievement : string) =
|
||||||
connStr
|
connStr
|
||||||
|> Sql.connect
|
|> Sql.connect
|
||||||
|> Sql.query (
|
|> Sql.query
|
||||||
"WITH ach AS (INSERT INTO achievement (symbol) VALUES (@symbol) RETURNING id),
|
("WITH ach AS (INSERT INTO achievement (symbol) VALUES (@symbol) RETURNING id),
|
||||||
usr AS (SELECT id FROM \"user\" WHERE discord_id = @did)
|
usr AS (SELECT id FROM \"user\" WHERE discord_id = @did)
|
||||||
INSERT INTO user_achievements_achievement (user_id, achievement_id) SELECT usr.id, ach.id FROM usr, ach")
|
INSERT INTO user_achievements_achievement (user_id, achievement_id) SELECT usr.id, ach.id FROM usr, ach")
|
||||||
|
|
||||||
|
@ -88,23 +88,26 @@ module Types =
|
|||||||
with static member empty = { Id = 0uL ; Name = "None" }
|
with static member empty = { Id = 0uL ; Name = "None" }
|
||||||
|
|
||||||
type PlayerEventResult =
|
type PlayerEventResult =
|
||||||
| Positive = 0
|
| Positive
|
||||||
| Neutral = 1
|
| Neutral
|
||||||
| Negative = 2
|
| Negative
|
||||||
|
|
||||||
|
type HackEvent = {
|
||||||
|
IsInstigator : bool
|
||||||
|
Adversary : DiscordPlayer
|
||||||
|
Result : PlayerEventResult
|
||||||
|
HackId : int
|
||||||
|
}
|
||||||
|
|
||||||
type PlayerEventType =
|
type PlayerEventType =
|
||||||
| Hacking = 0
|
| Hacking of HackEvent
|
||||||
| Shielding = 1
|
| Shielding of shieldId : int
|
||||||
| Steal = 2
|
| Stealing of instigator : bool * adversary : DiscordPlayer
|
||||||
| Imprison = 3
|
| Imprison
|
||||||
|
|
||||||
[<CLIMutable>]
|
[<CLIMutable>]
|
||||||
type PlayerEvent =
|
type PlayerEvent =
|
||||||
{ Type : PlayerEventType
|
{ Type : PlayerEventType
|
||||||
Result : PlayerEventResult
|
|
||||||
IsInstigator : bool
|
|
||||||
Adversary : DiscordPlayer
|
|
||||||
ItemId : int
|
|
||||||
Cooldown : int<mins>
|
Cooldown : int<mins>
|
||||||
Timestamp : DateTime }
|
Timestamp : DateTime }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user