mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-16 10:48:00 +00:00
Merge branch 'master' into 'master'
[Bug] - Allow games that throw exceptions or timer runs out to allow GameOutcomes to return with a NPE being thrown See merge request core-developers/forge!3264
This commit is contained in:
@@ -6,12 +6,12 @@
|
|||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
@@ -33,7 +33,7 @@ import java.util.Map.Entry;
|
|||||||
* <p>
|
* <p>
|
||||||
* GameInfo class.
|
* GameInfo class.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @author Forge
|
* @author Forge
|
||||||
* @version $Id: GameOutcome.java 17608 2012-10-20 22:27:27Z Max mtg $
|
* @version $Id: GameOutcome.java 17608 2012-10-20 22:27:27Z Max mtg $
|
||||||
*/
|
*/
|
||||||
@@ -47,7 +47,7 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
|
|
||||||
public final List<PaperCard> lostCards;
|
public final List<PaperCard> lostCards;
|
||||||
public final List<PaperCard> wonCards;
|
public final List<PaperCard> wonCards;
|
||||||
|
|
||||||
private AnteResult(List<PaperCard> cards, boolean won) {
|
private AnteResult(List<PaperCard> cards, boolean won) {
|
||||||
// Need empty lists for other results for addition of change ownership cards
|
// Need empty lists for other results for addition of change ownership cards
|
||||||
if (won) {
|
if (won) {
|
||||||
@@ -67,15 +67,20 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
this.lostCards.addAll(cards);
|
this.lostCards.addAll(cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AnteResult won(List<PaperCard> cards) { return new AnteResult(cards, true); }
|
public static AnteResult won(List<PaperCard> cards) {
|
||||||
public static AnteResult lost(List<PaperCard> cards) { return new AnteResult(cards, false); }
|
return new AnteResult(cards, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnteResult lost(List<PaperCard> cards) {
|
||||||
|
return new AnteResult(cards, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int lastTurnNumber = 0;
|
private int lastTurnNumber = 0;
|
||||||
private int lifeDelta = 0;
|
private int lifeDelta = 0;
|
||||||
private int winningTeam = -1;
|
private int winningTeam = -1;
|
||||||
|
|
||||||
private final HashMap<RegisteredPlayer, PlayerStatistics> playerRating = new HashMap<>();
|
private final HashMap<RegisteredPlayer, PlayerStatistics> playerRating = new HashMap<>();
|
||||||
private final HashMap<RegisteredPlayer, String> playerNames = new HashMap<>();
|
private final HashMap<RegisteredPlayer, String> playerNames = new HashMap<>();
|
||||||
|
|
||||||
public final Map<RegisteredPlayer, AnteResult> anteResult = new HashMap<>();
|
public final Map<RegisteredPlayer, AnteResult> anteResult = new HashMap<>();
|
||||||
@@ -83,21 +88,22 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
|
|
||||||
public GameOutcome(GameEndReason reason, final Iterable<Player> players) {
|
public GameOutcome(GameEndReason reason, final Iterable<Player> players) {
|
||||||
winCondition = reason;
|
winCondition = reason;
|
||||||
calculateLifeDelta(players);
|
|
||||||
|
|
||||||
int winnersHealth = 0;
|
|
||||||
int opponentsHealth = 0;
|
|
||||||
|
|
||||||
for (final Player p : players) {
|
for (final Player p : players) {
|
||||||
this.playerRating.put(p.getRegisteredPlayer(), p.getStats());
|
this.playerRating.put(p.getRegisteredPlayer(), p.getStats());
|
||||||
this.playerNames.put(p.getRegisteredPlayer(), p.getName());
|
this.playerNames.put(p.getRegisteredPlayer(), p.getName());
|
||||||
|
|
||||||
if (p.getOutcome().hasWon() && winCondition == GameEndReason.AllOpposingTeamsLost) {
|
if (winCondition == GameEndReason.AllOpposingTeamsLost && p.getOutcome().hasWon()) {
|
||||||
// Only mark the WinningTeam when "Team mode" is on.
|
// Only mark the WinningTeam when "Team mode" is on.
|
||||||
winningTeam = p.getTeam();
|
winningTeam = p.getTeam();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unable to calculate lifeDelta between a winning and losing player whe a draw is in place
|
||||||
|
if (winCondition == GameEndReason.Draw) return;
|
||||||
|
|
||||||
|
int winnersHealth = 0;
|
||||||
|
int opponentsHealth = 0;
|
||||||
for (final Player p : players) {
|
for (final Player p : players) {
|
||||||
if (p.getTeam() == winningTeam) {
|
if (p.getTeam() == winningTeam) {
|
||||||
winnersHealth += p.getLife();
|
winnersHealth += p.getLife();
|
||||||
@@ -106,22 +112,22 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculateLifeDelta(players);
|
||||||
lifeDelta = Math.max(0, winnersHealth - opponentsHealth);
|
lifeDelta = Math.max(0, winnersHealth - opponentsHealth);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void calculateLifeDelta(Iterable<Player> players) {
|
private void calculateLifeDelta(Iterable<Player> players) {
|
||||||
int opponentsHealth = 0;
|
int opponentsHealth = 0;
|
||||||
int winnersHealth = 0;
|
int winnersHealth = 0;
|
||||||
|
|
||||||
for (Player p : players) {
|
for (Player p : players) {
|
||||||
if (p.getOutcome().hasWon()) {
|
if (p.getOutcome().hasWon()) {
|
||||||
winnersHealth += p.getLife();
|
winnersHealth += p.getLife();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
opponentsHealth += p.getLife();
|
opponentsHealth += p.getLife();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifeDelta = Math.max(0, winnersHealth - opponentsHealth);
|
lifeDelta = Math.max(0, winnersHealth - opponentsHealth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +156,7 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the winner.
|
* Gets the winner.
|
||||||
*
|
*
|
||||||
* @return the winner
|
* @return the winner
|
||||||
*/
|
*/
|
||||||
public LobbyPlayer getWinningLobbyPlayer() {
|
public LobbyPlayer getWinningLobbyPlayer() {
|
||||||
@@ -169,7 +175,7 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
* distinguish between human player names (a problem for hotseat games).
|
* distinguish between human player names (a problem for hotseat games).
|
||||||
*/
|
*/
|
||||||
public RegisteredPlayer getWinningPlayer() {
|
public RegisteredPlayer getWinningPlayer() {
|
||||||
for(Entry<RegisteredPlayer, PlayerStatistics> pair : playerRating.entrySet()) {
|
for (Entry<RegisteredPlayer, PlayerStatistics> pair : playerRating.entrySet()) {
|
||||||
if (pair.getValue().getOutcome().hasWon()) {
|
if (pair.getValue().getOutcome().hasWon()) {
|
||||||
return pair.getKey();
|
return pair.getKey();
|
||||||
}
|
}
|
||||||
@@ -196,7 +202,7 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the win spell effect.
|
* Gets the win spell effect.
|
||||||
*
|
*
|
||||||
* @return the win spell effect
|
* @return the win spell effect
|
||||||
*/
|
*/
|
||||||
public String getWinSpellEffect() {
|
public String getWinSpellEffect() {
|
||||||
@@ -227,7 +233,7 @@ public final class GameOutcome implements Iterable<Entry<RegisteredPlayer, Playe
|
|||||||
|
|
||||||
public List<String> getOutcomeStrings() {
|
public List<String> getOutcomeStrings() {
|
||||||
List<String> outcomes = Lists.newArrayList();
|
List<String> outcomes = Lists.newArrayList();
|
||||||
for(RegisteredPlayer player : playerNames.keySet()) {
|
for (RegisteredPlayer player : playerNames.keySet()) {
|
||||||
outcomes.add(getOutcomeString(player));
|
outcomes.add(getOutcomeString(player));
|
||||||
}
|
}
|
||||||
return outcomes;
|
return outcomes;
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
package forge.view;
|
package forge.view;
|
||||||
|
|
||||||
|
import forge.LobbyPlayer;
|
||||||
|
import forge.deck.Deck;
|
||||||
|
import forge.deck.DeckGroup;
|
||||||
|
import forge.deck.io.DeckSerializer;
|
||||||
|
import forge.game.*;
|
||||||
|
import forge.game.player.RegisteredPlayer;
|
||||||
|
import forge.model.FModel;
|
||||||
|
import forge.player.GamePlayerUtil;
|
||||||
|
import forge.properties.ForgeConstants;
|
||||||
|
import forge.tournament.system.*;
|
||||||
|
import forge.util.Lang;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
import forge.util.WordUtil;
|
||||||
|
import forge.util.storage.IStorage;
|
||||||
|
import org.apache.commons.lang3.time.StopWatch;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import forge.LobbyPlayer;
|
|
||||||
import forge.deck.DeckGroup;
|
|
||||||
import forge.game.*;
|
|
||||||
import forge.properties.ForgeConstants;
|
|
||||||
import forge.tournament.system.*;
|
|
||||||
import forge.util.TextUtil;
|
|
||||||
import forge.util.WordUtil;
|
|
||||||
import forge.util.storage.IStorage;
|
|
||||||
import org.apache.commons.lang3.time.StopWatch;
|
|
||||||
|
|
||||||
import forge.deck.Deck;
|
|
||||||
import forge.deck.io.DeckSerializer;
|
|
||||||
import forge.game.player.RegisteredPlayer;
|
|
||||||
import forge.model.FModel;
|
|
||||||
import forge.player.GamePlayerUtil;
|
|
||||||
import forge.util.Lang;
|
|
||||||
|
|
||||||
public class SimulateMatch {
|
public class SimulateMatch {
|
||||||
public static void simulate(String[] args) {
|
public static void simulate(String[] args) {
|
||||||
FModel.initialize(null, null);
|
FModel.initialize(null, null);
|
||||||
|
|
||||||
System.out.println("Simulation mode");
|
System.out.println("Simulation mode");
|
||||||
if(args.length < 4) {
|
if (args.length < 4) {
|
||||||
argumentHelp();
|
argumentHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -49,11 +48,9 @@ public class SimulateMatch {
|
|||||||
|
|
||||||
options = new ArrayList<>();
|
options = new ArrayList<>();
|
||||||
params.put(a.substring(1), options);
|
params.put(a.substring(1), options);
|
||||||
}
|
} else if (options != null) {
|
||||||
else if (options != null) {
|
|
||||||
options.add(a);
|
options.add(a);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
System.err.println("Illegal parameter usage");
|
System.err.println("Illegal parameter usage");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,7 +94,7 @@ public class SimulateMatch {
|
|||||||
int i = 1;
|
int i = 1;
|
||||||
|
|
||||||
if (params.containsKey("d")) {
|
if (params.containsKey("d")) {
|
||||||
for(String deck : params.get("d")) {
|
for (String deck : params.get("d")) {
|
||||||
Deck d = deckFromCommandLineParameter(deck, type);
|
Deck d = deckFromCommandLineParameter(deck, type);
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
System.out.println(TextUtil.concatNoSpace("Could not load deck - ", deck, ", match cannot start"));
|
System.out.println(TextUtil.concatNoSpace("Could not load deck - ", deck, ", match cannot start"));
|
||||||
@@ -130,7 +127,7 @@ public class SimulateMatch {
|
|||||||
|
|
||||||
if (matchSize != 0) {
|
if (matchSize != 0) {
|
||||||
int iGame = 0;
|
int iGame = 0;
|
||||||
while(!mc.isMatchOver()) {
|
while (!mc.isMatchOver()) {
|
||||||
// play games until the match ends
|
// play games until the match ends
|
||||||
simulateSingleMatch(mc, iGame, outputGamelog);
|
simulateSingleMatch(mc, iGame, outputGamelog);
|
||||||
iGame++;
|
iGame++;
|
||||||
@@ -159,38 +156,26 @@ public class SimulateMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void simulateSingleMatch(final Match mc, int iGame, boolean outputGamelog) {
|
public static void simulateSingleMatch(final Match mc, int iGame, boolean outputGamelog) {
|
||||||
final StopWatch sw = new StopWatch();
|
final StopWatch sw = new StopWatch();
|
||||||
sw.start();
|
sw.start();
|
||||||
|
|
||||||
final Game g1 = mc.createGame();
|
final Game g1 = mc.createGame();
|
||||||
// will run match in the same thread
|
// will run match in the same thread
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
try {
|
try {
|
||||||
TimeLimitedCodeBlock.runWithTimeout(new Runnable() {
|
TimeLimitedCodeBlock.runWithTimeout(() -> {
|
||||||
@Override
|
mc.startGame(g1);
|
||||||
public void run() {
|
sw.stop();
|
||||||
mc.startGame(g1);
|
|
||||||
sw.stop();
|
|
||||||
}
|
|
||||||
}, 120, TimeUnit.SECONDS);
|
}, 120, TimeUnit.SECONDS);
|
||||||
}
|
} catch (TimeoutException e) {
|
||||||
catch (TimeoutException e) {
|
|
||||||
System.out.println("Stopping slow match as draw");
|
System.out.println("Stopping slow match as draw");
|
||||||
g1.setGameOver(GameEndReason.Draw);
|
} catch (Exception | StackOverflowError e) {
|
||||||
sw.stop();
|
|
||||||
}catch (Exception e){
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
g1.setGameOver(GameEndReason.Draw);
|
} finally {
|
||||||
sw.stop();
|
|
||||||
}catch(StackOverflowError e){
|
|
||||||
g1.setGameOver(GameEndReason.Draw);
|
g1.setGameOver(GameEndReason.Draw);
|
||||||
sw.stop();
|
sw.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
List<GameLogEntry> log;
|
List<GameLogEntry> log;
|
||||||
if (outputGamelog) {
|
if (outputGamelog) {
|
||||||
log = g1.getGameLog().getLogEntries(null);
|
log = g1.getGameLog().getLogEntries(null);
|
||||||
@@ -198,15 +183,15 @@ public class SimulateMatch {
|
|||||||
log = g1.getGameLog().getLogEntries(GameLogEntryType.MATCH_RESULTS);
|
log = g1.getGameLog().getLogEntries(GameLogEntryType.MATCH_RESULTS);
|
||||||
}
|
}
|
||||||
Collections.reverse(log);
|
Collections.reverse(log);
|
||||||
for(GameLogEntry l : log) {
|
for (GameLogEntry l : log) {
|
||||||
System.out.println(l);
|
System.out.println(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both players life totals to 0 in a single turn, the game should end in a draw
|
// If both players life totals to 0 in a single turn, the game should end in a draw
|
||||||
if(g1.getOutcome().isDraw()){
|
if (g1.getOutcome().isDraw()) {
|
||||||
System.out.println(String.format("Game %d ended in a Draw! Took %d ms.", 1+iGame, sw.getTime()));
|
System.out.printf("\nGame Result: Game %d ended in a Draw! Took %d ms.%n", 1 + iGame, sw.getTime());
|
||||||
} else {
|
} else {
|
||||||
System.out.println(String.format("\nGame %d ended in %d ms. %s has won!\n", 1+iGame, sw.getTime(), g1.getOutcome().getWinningLobbyPlayer().getName()));
|
System.out.printf("\nGame Result: Game %d ended in %d ms. %s has won!\n%n", 1 + iGame, sw.getTime(), g1.getOutcome().getWinningLobbyPlayer().getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +204,7 @@ public class SimulateMatch {
|
|||||||
List<TournamentPlayer> players = new ArrayList<>();
|
List<TournamentPlayer> players = new ArrayList<>();
|
||||||
int numPlayers = 0;
|
int numPlayers = 0;
|
||||||
if (params.containsKey("d")) {
|
if (params.containsKey("d")) {
|
||||||
for(String deck : params.get("d")) {
|
for (String deck : params.get("d")) {
|
||||||
Deck d = deckFromCommandLineParameter(deck, rules.getGameType());
|
Deck d = deckFromCommandLineParameter(deck, rules.getGameType());
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
System.out.println(TextUtil.concatNoSpace("Could not load deck - ", deck, ", match cannot start"));
|
System.out.println(TextUtil.concatNoSpace("Could not load deck - ", deck, ", match cannot start"));
|
||||||
@@ -239,7 +224,7 @@ public class SimulateMatch {
|
|||||||
if (!folder.isDirectory()) {
|
if (!folder.isDirectory()) {
|
||||||
System.out.println("Directory not found - " + foldName);
|
System.out.println("Directory not found - " + foldName);
|
||||||
} else {
|
} else {
|
||||||
for(File deck : folder.listFiles(new FilenameFilter() {
|
for (File deck : folder.listFiles(new FilenameFilter() {
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(File dir, String name) {
|
public boolean accept(File dir, String name) {
|
||||||
return name.endsWith(".dck");
|
return name.endsWith(".dck");
|
||||||
@@ -281,16 +266,16 @@ public class SimulateMatch {
|
|||||||
System.out.println(TextUtil.concatNoSpace("Starting a ", tournament, " tournament with ",
|
System.out.println(TextUtil.concatNoSpace("Starting a ", tournament, " tournament with ",
|
||||||
String.valueOf(numPlayers), " players over ",
|
String.valueOf(numPlayers), " players over ",
|
||||||
String.valueOf(tourney.getTotalRounds()), " rounds"));
|
String.valueOf(tourney.getTotalRounds()), " rounds"));
|
||||||
while(!tourney.isTournamentOver()) {
|
while (!tourney.isTournamentOver()) {
|
||||||
if (tourney.getActiveRound() != curRound) {
|
if (tourney.getActiveRound() != curRound) {
|
||||||
if (curRound != 0) {
|
if (curRound != 0) {
|
||||||
System.out.println(TextUtil.concatNoSpace("End Round - ", String.valueOf(curRound)));
|
System.out.println(TextUtil.concatNoSpace("End Round - ", String.valueOf(curRound)));
|
||||||
}
|
}
|
||||||
curRound = tourney.getActiveRound();
|
curRound = tourney.getActiveRound();
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println(TextUtil.concatNoSpace("Round ", String.valueOf(curRound) ," Pairings:"));
|
System.out.println(TextUtil.concatNoSpace("Round ", String.valueOf(curRound), " Pairings:"));
|
||||||
|
|
||||||
for(TournamentPairing pairing : tourney.getActivePairings()) {
|
for (TournamentPairing pairing : tourney.getActivePairings()) {
|
||||||
System.out.println(pairing.outputHeader());
|
System.out.println(pairing.outputHeader());
|
||||||
}
|
}
|
||||||
System.out.println();
|
System.out.println();
|
||||||
@@ -311,10 +296,10 @@ public class SimulateMatch {
|
|||||||
int iGame = 0;
|
int iGame = 0;
|
||||||
while (!mc.isMatchOver()) {
|
while (!mc.isMatchOver()) {
|
||||||
// play games until the match ends
|
// play games until the match ends
|
||||||
try{
|
try {
|
||||||
simulateSingleMatch(mc, iGame, outputGamelog);
|
simulateSingleMatch(mc, iGame, outputGamelog);
|
||||||
iGame++;
|
iGame++;
|
||||||
} catch(Exception e) {
|
} catch (Exception e) {
|
||||||
exceptions++;
|
exceptions++;
|
||||||
System.out.println(e.toString());
|
System.out.println(e.toString());
|
||||||
if (exceptions > 5) {
|
if (exceptions > 5) {
|
||||||
@@ -349,10 +334,10 @@ public class SimulateMatch {
|
|||||||
|
|
||||||
private static Deck deckFromCommandLineParameter(String deckname, GameType type) {
|
private static Deck deckFromCommandLineParameter(String deckname, GameType type) {
|
||||||
int dotpos = deckname.lastIndexOf('.');
|
int dotpos = deckname.lastIndexOf('.');
|
||||||
if(dotpos > 0 && dotpos == deckname.length()-4) {
|
if (dotpos > 0 && dotpos == deckname.length() - 4) {
|
||||||
String baseDir = type.equals(GameType.Commander) ?
|
String baseDir = type.equals(GameType.Commander) ?
|
||||||
ForgeConstants.DECK_COMMANDER_DIR : ForgeConstants.DECK_CONSTRUCTED_DIR;
|
ForgeConstants.DECK_COMMANDER_DIR : ForgeConstants.DECK_CONSTRUCTED_DIR;
|
||||||
return DeckSerializer.fromFile(new File(baseDir+deckname));
|
return DeckSerializer.fromFile(new File(baseDir + deckname));
|
||||||
}
|
}
|
||||||
|
|
||||||
IStorage<Deck> deckStore = null;
|
IStorage<Deck> deckStore = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user