refactor: move event checks from builder to their respective classes
This commit is contained in:
parent
d36466a5a0
commit
80c77b7956
@ -47,6 +47,50 @@ public class CharacterEvent extends Event {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
if(!super.check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
// Melee- and ranged attacks need the same properties (except for the type)
|
||||
case MeleeAttackEvent:
|
||||
case RangedAttackEvent:
|
||||
if (this.originField == null ||
|
||||
this.targetField == null ||
|
||||
this.originEntity == null ||
|
||||
this.targetEntity == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// Exchanging and using InfinityStones both uses the same keys. Hereby, using a stone like the
|
||||
// RealityStone causes the stone to be used on oneself
|
||||
case ExchangeInfinityStoneEvent:
|
||||
case UseInfinityStoneEvent:
|
||||
if (this.originField == null ||
|
||||
this.targetField == null ||
|
||||
this.originEntity == null ||
|
||||
this.targetEntity == null ||
|
||||
this.stoneType == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// MoveEvents take an originField, a targetField, and an originEntity.
|
||||
case MoveEvent:
|
||||
if (this.originField == null ||
|
||||
this.targetField == null ||
|
||||
this.originEntity == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -8,6 +8,24 @@ public class CustomEvent extends Event {
|
||||
public String teamIdentifier;
|
||||
public HashMap<String, Object> customContent;
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
if(!super.check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
// CustomEvent only requires the custom data.
|
||||
case CustomEvent:
|
||||
if (this.customContent == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -28,6 +28,43 @@ public class EntityEvent extends Event {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
if(!super.check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
// DestroyedEntityEvent takes an ID and a field, and destroys the entity if the field is correct
|
||||
case DestroyedEntityEvent:
|
||||
if (this.targetField == null || this.targetEntity == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// TakenDamage, ConsumedAP / MP and Healed all need a targetField, targetEntity and Amount.
|
||||
case TakenDamageEvent:
|
||||
case ConsumedAPEvent:
|
||||
case ConsumedMPEvent:
|
||||
case HealedEvent:
|
||||
if (this.targetField == null ||
|
||||
this.targetEntity == null ||
|
||||
this.amount == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// SpawnEntity needs an entity, of course.
|
||||
case SpawnEntityEvent:
|
||||
if (this.entity == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -13,6 +13,14 @@ public abstract class Event {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the event contains all necessary properties according to its {@link EventType}.
|
||||
* @return True if the event has all required properties set, otherwise false
|
||||
*/
|
||||
public boolean check() {
|
||||
return type != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -6,7 +6,6 @@ import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
|
||||
import uulm.teamname.marvelous.gamelibrary.entities.StoneType;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
@ -166,153 +165,6 @@ public class EventBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A method to check whether the current event is actually built correctly. If that is not the case, it
|
||||
* throws an {@link IllegalStateException}.
|
||||
* This occurs if for example a property is null even though it shouldn't be.
|
||||
* The check is based on the EventType. <b>Using this method is strongly recommended when working with
|
||||
* entities, as it prevents unnoticed bugs before they might happen!</b>
|
||||
*
|
||||
* @throws IllegalStateException if the current event is non-valid
|
||||
*/
|
||||
public EventBuilder check() throws IllegalStateException {
|
||||
if (this.type == null) throw new IllegalStateException("The eventType is null");
|
||||
else {
|
||||
switch (this.type) {
|
||||
// all of those events do not need any extra values except the EventType
|
||||
case Ack:
|
||||
case Nack:
|
||||
case Req:
|
||||
case PauseStartEvent:
|
||||
case PauseStopEvent:
|
||||
case TurnTimeoutEvent:
|
||||
case DisconnectEvent:
|
||||
break;
|
||||
|
||||
// GameState needs very specific properties
|
||||
case GameStateEvent:
|
||||
if (this.entities == null ||
|
||||
this.turnOrder == null ||
|
||||
this.activeCharacter == null ||
|
||||
this.winCondition == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// CustomEvent needs only... well, CustomContent. Who would've thought!
|
||||
case CustomEvent:
|
||||
if (this.customContent == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// DestroyedEntityEvent takes an ID and a field, and destroys the entity if the field is correct
|
||||
case DestroyedEntityEvent:
|
||||
if (this.targetField == null || this.targetEntity == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// TakenDamage, ConsumedAP / MP and Healed all need a targetField, targetEntity and Amount.
|
||||
case TakenDamageEvent:
|
||||
case ConsumedAPEvent:
|
||||
case ConsumedMPEvent:
|
||||
case HealedEvent:
|
||||
if (this.targetField == null ||
|
||||
this.targetEntity == null ||
|
||||
this.amount == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// SpawnEntity needs an entity, of course.
|
||||
case SpawnEntityEvent:
|
||||
if (this.entity == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
// Melee- and ranged attacks need the same properties (except for the type)
|
||||
case MeleeAttackEvent:
|
||||
case RangedAttackEvent:
|
||||
if (this.originField == null ||
|
||||
this.targetField == null ||
|
||||
this.originEntity == null ||
|
||||
this.targetEntity == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// Exchanging and using InfinityStones both uses the same keys. Hereby, using a stone like the
|
||||
// RealityStone causes the stone to be used on oneself
|
||||
case ExchangeInfinityStoneEvent:
|
||||
case UseInfinityStoneEvent:
|
||||
if (this.originField == null ||
|
||||
this.targetField == null ||
|
||||
this.originEntity == null ||
|
||||
this.targetEntity == null ||
|
||||
this.stoneType == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// MoveEvents take an originField, a targetField, and an originEntity.
|
||||
case MoveEvent:
|
||||
if (this.originField == null ||
|
||||
this.targetField == null ||
|
||||
this.originEntity == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// RoundSetupEvents take a RoundCount and a CharacterOrder
|
||||
case RoundSetupEvent:
|
||||
if (this.roundCount == null || this.characterOrder == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// TurnEvents take a TurnCount and a NextCharacter
|
||||
case TurnEvent:
|
||||
if (this.turnCount == null || this.nextCharacter == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// A WinEvent needs to know what player has won
|
||||
case WinEvent:
|
||||
if (this.playerWon == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// TimeoutEvents give a message back. As the only events to do so, this might be removed later on.
|
||||
case TimeoutEvent:
|
||||
if (this.message == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
|
||||
// TimeoutWarnings carry a message and the amount of time left in seconds. The message might disappear.
|
||||
case TimeoutWarningEvent:
|
||||
if (this.message == null || this.timeLeft == null) {
|
||||
throwException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for throwing a specific exception.
|
||||
* @throws IllegalStateException if the builder hasn't received enough properties to construct the event
|
||||
*/
|
||||
private void throwException() throws IllegalStateException {
|
||||
throw new IllegalStateException("Properties malformed for " + this.type + ".\n" + "Builder properties: " + this.notNullToString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link GameEvent} from the values given to the builder.
|
||||
* <ul>
|
||||
@ -326,21 +178,9 @@ public class EventBuilder {
|
||||
* <li>timeLeft describes the time left for a client to act before getting kicked in the TimeoutWarning event</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param checked Determines if properties should be checked
|
||||
* @return a {@link GameEvent} based on the builder
|
||||
*/
|
||||
public GameEvent buildGameEvent(boolean checked) throws IllegalStateException {
|
||||
if(checked) {
|
||||
this.check();
|
||||
}
|
||||
return buildGameEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link GameEvent} from the values given to the builder without checking.
|
||||
* @return a {@link GameEvent} based on the builder
|
||||
*/
|
||||
private GameEvent buildGameEvent() {
|
||||
public GameEvent buildGameEvent() throws IllegalStateException {
|
||||
var gameEvent = new GameEvent();
|
||||
gameEvent.type = this.type;
|
||||
gameEvent.roundCount = this.roundCount;
|
||||
@ -364,21 +204,9 @@ public class EventBuilder {
|
||||
* <li>amount is a generic amount, for example damage taken</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param checked Determines if properties should be checked
|
||||
* @return an {@link EntityEvent} based on the builder
|
||||
*/
|
||||
public EntityEvent buildEntityEvent(boolean checked) throws IllegalStateException {
|
||||
if(checked) {
|
||||
this.check();
|
||||
}
|
||||
return this.buildEntityEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an {@link EntityEvent} from the values given to the builder without checking.
|
||||
* @return an {@link EntityEvent} based on the builder
|
||||
*/
|
||||
private EntityEvent buildEntityEvent() {
|
||||
public EntityEvent buildEntityEvent() throws IllegalStateException {
|
||||
var entityEvent = new EntityEvent();
|
||||
entityEvent.type = this.type;
|
||||
entityEvent.targetEntity = this.targetEntity;
|
||||
@ -398,21 +226,9 @@ public class EventBuilder {
|
||||
* <li>targetField is the target field in the form of an {@link IntVector2}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param checked Determines if properties should be checked
|
||||
* @return a {@link CharacterEvent} based on the builder
|
||||
*/
|
||||
public CharacterEvent buildCharacterEvent(boolean checked) throws IllegalStateException {
|
||||
if(checked) {
|
||||
this.check();
|
||||
}
|
||||
return buildCharacterEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link CharacterEvent} from the values given to the builder without checking.
|
||||
* @return a {@link CharacterEvent} based on the builder
|
||||
*/
|
||||
private CharacterEvent buildCharacterEvent() {
|
||||
public CharacterEvent buildCharacterEvent() throws IllegalStateException {
|
||||
var characterEvent = new CharacterEvent();
|
||||
characterEvent.type = this.type;
|
||||
characterEvent.originEntity = this.originEntity;
|
||||
@ -434,21 +250,9 @@ public class EventBuilder {
|
||||
* <li>describes whether the win condition is in effect</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param checked Determines if properties should be checked
|
||||
* @return a {@link GameStateEvent} based on the builder
|
||||
*/
|
||||
public GameStateEvent buildGameStateEvent(boolean checked) throws IllegalStateException {
|
||||
if(checked) {
|
||||
this.check();
|
||||
}
|
||||
return this.buildGameStateEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link GameStateEvent} from the values given to the builder without checking.
|
||||
* @return a {@link GameStateEvent} based on the builder
|
||||
*/
|
||||
private GameStateEvent buildGameStateEvent() {
|
||||
public GameStateEvent buildGameStateEvent() throws IllegalStateException {
|
||||
var gameStateEvent = new GameStateEvent();
|
||||
gameStateEvent.type = this.type;
|
||||
gameStateEvent.entities = this.entities;
|
||||
@ -465,21 +269,9 @@ public class EventBuilder {
|
||||
* <li>customContent is a {@link HashMap}<{@link String}, {@link Object}> resembling the JSON keys in the event</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param checked Determines if properties should be checked
|
||||
* @return a {@link CustomEvent} based on the builder
|
||||
*/
|
||||
public CustomEvent buildCustomEvent(boolean checked) throws IllegalStateException {
|
||||
if(checked) {
|
||||
this.check();
|
||||
}
|
||||
return this.buildCustomEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link CustomEvent} from the values given to the builder without checking.
|
||||
* @return a {@link CustomEvent} based on the builder
|
||||
*/
|
||||
private CustomEvent buildCustomEvent() {
|
||||
public CustomEvent buildCustomEvent() throws IllegalStateException {
|
||||
var customEvent = new CustomEvent();
|
||||
customEvent.type = this.type;
|
||||
customEvent.teamIdentifier = this.teamIdentifier;
|
||||
|
@ -17,6 +17,59 @@ public class GameEvent extends Event {
|
||||
public String message;
|
||||
public Integer timeLeft;
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
if(!super.check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
// all of those events do not need any extra values except the EventType
|
||||
case PauseStartEvent:
|
||||
case PauseStopEvent:
|
||||
case TurnTimeoutEvent:
|
||||
case DisconnectEvent:
|
||||
break;
|
||||
|
||||
// RoundSetupEvents take a RoundCount and a CharacterOrder
|
||||
case RoundSetupEvent:
|
||||
if (this.roundCount == null || this.characterOrder == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// TurnEvents take a TurnCount and a NextCharacter
|
||||
case TurnEvent:
|
||||
if (this.turnCount == null || this.nextCharacter == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// A WinEvent needs to know what player has won
|
||||
case WinEvent:
|
||||
if (this.playerWon == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// TimeoutEvents give a message back. As the only events to do so, this might be removed later on.
|
||||
case TimeoutEvent:
|
||||
if (this.message == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// TimeoutWarnings carry a message and the amount of time left in seconds. The message might disappear.
|
||||
case TimeoutWarningEvent:
|
||||
if (this.message == null || this.timeLeft == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -13,6 +13,27 @@ public class GameStateEvent extends Event {
|
||||
public EntityID activeCharacter;
|
||||
public Boolean winCondition;
|
||||
|
||||
@Override
|
||||
public boolean check() {
|
||||
if(!super.check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
// GameState needs very specific properties
|
||||
case GameStateEvent:
|
||||
if (this.entities == null ||
|
||||
this.turnOrder == null ||
|
||||
this.activeCharacter == null ||
|
||||
this.winCondition == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -37,7 +37,7 @@ class EventBuilderTest {
|
||||
new EntityID(EntityType.P2, 0),
|
||||
};
|
||||
|
||||
turn = new EntityID(EntityType.P1, 0);
|
||||
turn = turns[0];
|
||||
|
||||
entities = new Entity[]{
|
||||
new Character(
|
||||
@ -79,7 +79,6 @@ class EventBuilderTest {
|
||||
25);
|
||||
|
||||
filled = new EventBuilder(EventType.CustomEvent)
|
||||
// .withType(EventType.CustomEvent)
|
||||
.withTargetEntity(new EntityID(EntityType.P1, 1))
|
||||
.withTargetField(new IntVector2(11, 13))
|
||||
.withAmount(15)
|
||||
@ -126,7 +125,11 @@ class EventBuilderTest {
|
||||
var roundSetupEvent = new EventBuilder(EventType.RoundSetupEvent)
|
||||
.withRoundCount(4)
|
||||
.withCharacterOrder(turns)
|
||||
.buildGameEvent(true);
|
||||
.buildGameEvent();
|
||||
|
||||
assertThat(roundSetupEvent.check())
|
||||
.isTrue()
|
||||
.withFailMessage("RoundSetupEvent failed check");
|
||||
|
||||
var roundSetupEventBaseline = new GameEvent();
|
||||
roundSetupEventBaseline.type = EventType.RoundSetupEvent;
|
||||
@ -139,13 +142,17 @@ class EventBuilderTest {
|
||||
|
||||
var turnEvent = new EventBuilder(EventType.TurnEvent)
|
||||
.withNextCharacter(turn)
|
||||
.withRoundCount(5)
|
||||
.buildGameEvent(false);
|
||||
.withTurnCount(5)
|
||||
.buildGameEvent();
|
||||
|
||||
assertThat(turnEvent.check())
|
||||
.isTrue()
|
||||
.withFailMessage("TurnEvent failed check");
|
||||
|
||||
var turnEventBaseline = new GameEvent();
|
||||
turnEventBaseline.type = EventType.TurnEvent;
|
||||
turnEventBaseline.nextCharacter = turn;
|
||||
turnEventBaseline.roundCount = 5;
|
||||
turnEventBaseline.turnCount = 5;
|
||||
|
||||
assertThat(turnEvent)
|
||||
.isEqualTo(turnEventBaseline)
|
||||
@ -163,7 +170,11 @@ class EventBuilderTest {
|
||||
.withPlayerWon(5912)
|
||||
.withMessage("This message is very much not useful at all")
|
||||
.withTimeLeft(-144)
|
||||
.buildGameEvent(false);
|
||||
.buildGameEvent();
|
||||
|
||||
assertThat(gameEvent.check())
|
||||
.isTrue()
|
||||
.withFailMessage("GameEvent failed check");
|
||||
|
||||
var baseline = new GameEvent();
|
||||
baseline.type = EventType.DisconnectEvent;
|
||||
@ -188,7 +199,11 @@ class EventBuilderTest {
|
||||
.withTurnOrder(turns)
|
||||
.withActiveCharacter(turn)
|
||||
.withWinCondition(true)
|
||||
.buildGameStateEvent(false);
|
||||
.buildGameStateEvent();
|
||||
|
||||
assertThat(gameStateEvent.check())
|
||||
.isTrue()
|
||||
.withFailMessage("GameStateEvent failed check");
|
||||
|
||||
var baseline = new GameStateEvent();
|
||||
baseline.type = EventType.ConsumedAPEvent;
|
||||
@ -216,46 +231,42 @@ class EventBuilderTest {
|
||||
|
||||
@Test
|
||||
void buildGameStateEventWithTooManyProperties() {
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> new EventBuilder(EventType.Ack) // too many properties is fine
|
||||
assertThat(new EventBuilder(EventType.Ack) // too many properties is fine
|
||||
.withAmount(15) // also properties of different EventTypes, they just get ignored
|
||||
.withEntities(entities) // properties belonging to the same eventType get incorporated into
|
||||
.withWinCondition(false) // the final event, so they have to be ignored
|
||||
.buildGameStateEvent(true)); // by the programmer later on
|
||||
.buildGameStateEvent() // by the programmer later on
|
||||
.check()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildGameStateEvent() {
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> new EventBuilder(EventType.Ack) // needs no properties
|
||||
.buildGameStateEvent(true));
|
||||
assertThat(new EventBuilder(EventType.Ack) // needs no properties
|
||||
.buildGameStateEvent()
|
||||
.check()).isTrue();
|
||||
|
||||
|
||||
assertThat(new EventBuilder(EventType.Nack).buildGameStateEvent().check()).isTrue();
|
||||
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> new EventBuilder(EventType.Nack).buildGameStateEvent(true));
|
||||
assertThat(new EventBuilder(EventType.Req).buildGameStateEvent().check()).isTrue();
|
||||
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> new EventBuilder(EventType.Req).buildGameStateEvent(true));
|
||||
|
||||
assertThatExceptionOfType(IllegalStateException.class)
|
||||
.isThrownBy(() -> new EventBuilder(EventType.GameStateEvent) // if properties missing throw exception
|
||||
assertThat(new EventBuilder(EventType.GameStateEvent) // if properties missing throw exception
|
||||
.withTurnOrder(turns)
|
||||
.withActiveCharacter(turn)
|
||||
.buildGameStateEvent(true));
|
||||
.buildGameStateEvent()
|
||||
.check()).isFalse();
|
||||
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> new EventBuilder(EventType.GameStateEvent) // no exception if all properties present
|
||||
assertThat(new EventBuilder(EventType.GameStateEvent) // no exception if all properties present
|
||||
.withEntities(entities)
|
||||
.withTurnOrder(turns)
|
||||
.withActiveCharacter(turn)
|
||||
.withWinCondition(false)
|
||||
.buildGameStateEvent(true));
|
||||
.buildGameStateEvent()
|
||||
.check()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildCustomEvent() {
|
||||
// TODO: check CustomEvent validation for correctness
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user