Developing for Spellsource on Windows starts with installing dependencies, a good code editor, and familiarizing yourself with some common Java code practices.
Follow this guide to be able to test your cards and make changes to the game code on Windows 10 and later.
Table of Contents
1. Prerequisites
-
Install some helpful Windows development utilities.
- 7-Zip for a friendlier way to open zip files.
- ConEmu (download the installer) for a better console.
- Git for Windows which also installs some helpful console programs. Hit next on all the prompts, since it's a little confusing.
-
Install Java 12.
- Start by downloading the zip file for Windows (OpenJDK 12.0.1) from the OpenJDK website.
- Extract the zip file to your
C:\Program Files
directory. If you did this correctly, you should be able to findjava.exe
atC:\Program Files\jdk-12.0.1\bin\java.exe
.
- Install MongoDB 4.
-
Add
java
,git
andmongod
to your PATH:- Hit the Windows key to bring open the Start menu, and type "This PC".
- Right click on the This PC result and choose Properties. You should now see the
Control Panel\System and Security\System
control panel pane. - Click Advanced System Settings in the left sidebar.
- Click the Advanced Tab.
- Click the Environment Variables button.
- In the System variables pane, double click Path to edit it.
-
For each of the following paths, click the New button and set the text to the specified values below. You will make the below 3 entries, then click "OK."
C:\Program Files\jdk-12.0.1\bin
C:\Program Files\MongoDB\Server\4.0\bin
C:\Program Files\Git\bin
-
Back to the System variables pane, click "New..."
- In the "New System Variable" tab, enter
JAVA_HOME
as variable name, andC:\Program Files\jdk-12.0.1
as variable value.
- In the "New System Variable" tab, enter
- Install IntelliJ IDEA Community Edition to use as a code editor.
2. Download Spellsource
-
Fork the code on GitHub.
- Create a GitHub account or login with your existing one.
- Visit Spellsource-Server.
- Click Fork in the upper right corner to fork it into your account. This creates a copy of the game you can edit freely.
- In your fork's page, click clone or download, and copy the URL shown there. For example, if your username is
bdg
, you will see the URLhttps://github.com/bdg/Spellsource-Server
-
Open ConEmu.
- The first time you run it, you will be prompted to configure it. Under "Choose your startup task or even a shell with arguments:", choose
{Shells::PowerShell (Admin)}
. - Hit OK.
- You will now be in a console window that resembles
PS C:\Users\YourUsername>
with a blinking cursor.
- The first time you run it, you will be prompted to configure it. Under "Choose your startup task or even a shell with arguments:", choose
-
Enter commands to download the Spellsource code.
- First, "change directory" into your Documents folder with the command
cd .\Documents\
and hit enter. - Then, download the code by writing
git clone
(notice the space at the end), then pasting in the URL you copied from GitHub. If your username isbdg
on GitHub, the command will look like:git clone https://github.com/bdg/Spellsource-Server
. - Change directory into this code folder with the command
cd .\Spellsource-Server
. - Create a project file for IntelliJ with the command
./gradlew.bat idea
. This may take a while!
- First, "change directory" into your Documents folder with the command
3. Open the Project
- Start IntelliJ and hit next on all the prompts.
- Click Open, and navigate to your Spellsource-Server directory.
- Be very patient while it loads, which may take a while. IntelliJ's progress appears in the lower right corner.
-
Set your code style:
- Go to File > Settings.
- Navigate to Editor > Font.
- Change your font to Fira Code Retina. This will make text more legible.
- Again inside settings, navigate to Editor > Code Style.
- Click the gear icon to the right of Scheme, then choose Import Scheme > IntelliJ IDEA code style XML.
- Click the 3rd icon from the left above the file path, which looks like a folder with a mini IntelliJ IDEA logo in the lower right corner. This navigates you to the project folder.
- Choose idea-codestyle-scheme.xml in your project directory.
-
Configure IntelliJ to run the project correctly.
- Go to File > Settings.
- Navigate to Build, Execution, Deployment > Build Tools > Gradle.
- Under Delegate settings, both combo boxes should be set to Gradle.
- Navigate to Build, Execution, Deployment > Build Tools > Gradle > Runner.
- Check Delegate IDE build/run actions to Gradle.
- Set Run tests using: to Gradle Test Runner.
You have now configured a working Spellsource-Server editing environment.
4. Common Coding Tasks
Learn more about how the Spellsource engine works by exploring the documentation in the code or located here.
Any changes you make should be documented in the www/whatsnew.md
file. Open this file and edit the latest version with the appropriate fix or content addition notes.
Let's go over some common coding tasks to get you started with contributions.
4.1 Editing an Existing Card
- Make sure IntelliJ IDEA is open.
- Hit Shift twice to bring up the universal search, and enter the name of your card. In this example, I'll write Abholos.
- Observe there may be multiple results. Choose the file that appears to be a
.json
file located in thecards/
directory. In this case, we'd choose theminion_abholos.json
file. - Once you've made any changes, you may need to edit tests. Typically, the card's name is located in its test. Try hitting Shift twice and searching for Abholos. In this case, there is a
testAbholos
method. Written in Java, you will need to update this test. Run the test by clicking the green play button icon to the left of the test method declaration in the gutter of the editor.
Visit the documentation about CardDesc to learn how this card format works. You can browse the documentation to learn more about any specific effect. Search the word you see in the .json
file inside the search in the documentation.
4.2 Creating a New Card
- In the Project tool window showing all the files in the project in the left hand side of the editor, navigate to
cards/src/main/resources/cards/custom/group10
. If you don't see this pane, navigate to View > Tool Windows > Project. - Create a new
.json
file in this directory. -
Copy and paste the contents of a card similar to yours to get started.
- To find these cards, you can search the cards.
- Hit Ctrl+Shift+F to bring up the Find in Path window.
- Check the File Mask box, and write
*.json
to match only card code files. - Check the Regex box. This lets you make sophisticated text searches.
-
Searches will take the form of
"description": ".*keyword1.*keyword2.*morekeywords
.- For example, to find cards that give taunt, search
"description": ".*give.*taunt
. Observe before each keyword you write.*
, which signals to the regex search to allow any number of words in between your keywords. - To find cards that are an opener or a battlecry, search
"description": ".*(opener)|(battlecry)
. Observe the keywords are wrapped in parentheses and separated by a pipe character.
- For example, to find cards that give taunt, search
- Make sure the card's set line looks like
"set": "CUSTOM"
.
Use the complete reference here. In particular, the spells reference is handy for learning exactly how spells (effects) work.
Let's run through a complete example of implementing a card, "Exampler" that reads: Neutral (1) 4/4. Opener: Summon a 5/5 Skeleton for your opponent.
- In IntelliJ, create a file, minion_exampler.json, in the directory
cards/src/main/resources/cards/custom/group10
. - Find a similar card to start as a base. In this case, we'll search for cards that summon other cards. Let's use Rattling Rascal. Copy the contents of that card into
minion_exampler.json
. -
Edit the appropriate fields to create this card. My version is below:
{ "name": "Exampler", "baseManaCost": 1, "type": "MINION", "heroClass": "ANY", "baseAttack": 4, "baseHp": 4, "rarity": "EPIC", "description": "Opener: Summon a 5/5 Skeleton for your opponent", "battlecry": { "targetSelection": "NONE", "spell": { "class": "SummonSpell", "card": "token_skeletal_enforcer", "targetPlayer": "OPPONENT" } }, "attributes": { "BATTLECRY": true }, "collectible": true, "set": "CUSTOM", "fileFormatVersion": 1 }
-
Write a test that verifies that the card works. We'll create a new file, ExampleCardTests, that uses a "gym" to test that the card does what it is supposed to do. Here's an example test for Exampler:
package com.hiddenswitch.spellsource; import net.demilich.metastone.tests.util.TestBase; import org.testng.Assert; import org.testng.annotations.Test; public class ExampleCardTests extends TestBase { @Test public void testExampler() { runGym((context, player, opponent) -> { playCard(context, player, "minion_exampler"); Assert.assertEquals(opponent.getMinions().get(0).getSourceCard().getCardId(), "token_skeletal_enforcer", "The opponent should have a Skeletal Enforcer after Exampler is summoned"); }); } }
These tests can be as involved as you'd like, and should explore corner cases or interactions whenever possible. Many simple cards do not require tests. But when you start writing your own code to implement cards, tests are especially important to verify functionality. All community-contributed cards that get distributed to the production Spellsource server must have tests.
4.3 Creating a Custom Spell
Sometimes effects are too difficult to implement in the JSON scripting format and Java is better suited.
This example will implement the spell, "Summon the minion with the most copies in your deck."
-
Create a spell whose
"spell": {"class"...
iscustom.SummonMinionWithMostCopiesInDeckSpell
:{ "name": "A Common Summoner", "baseManaCost": 6, "type": "SPELL", "heroClass": "JADE", "rarity": "EPIC", "description": "Summon the minion with the most copies in your deck.", "targetSelection": "NONE", "spell": { "class": "custom.SummonMinionWithMostCopiesInDeckSpell" }, "collectible": true, "set": "CUSTOM", "fileFormatVersion": 1 }
-
Create a new Java file corresponding to this spell.
- In the Project panel, navigate to the
game/src/main/java/net/demilich/metastone/game/spells/custom/
directory by expanding the little triangles. - Right click on the directory icon with the white dot in it corresponding to
custom
. - Choose New > Java Class.
- Enter
SummonMinionWithMostCopiesInDeckSpell
- Write
extends Spell
after the class name. - The class will now appear to have a red underline underneath it. Hit Alt-Enter and choose Implement methods... Alt-Enter is a general hotkey for "Help me."
- Add the annotation
@Suspendable
toonCast
. - Write
/**
above thepublic class Summon...
line, and hit enter. You will now have autocompleted a comment block where you should document what this spell does. - Hit Ctrl-Alt-L to autoformat the file.
-
Your code will now look like this:
package net.demilich.metastone.game.spells.custom; import co.paralleluniverse.fibers.Suspendable; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.entities.Entity; import net.demilich.metastone.game.spells.Spell; import net.demilich.metastone.game.spells.desc.SpellDesc; /** * Summons a minion from the player's deck with the most copies in the deck. If there are multiple minions with the most * copies, summon one at random. */ public class SummonMinionWithMostCopiesInDeckSpell extends Spell { @Override @Suspendable protected void onCast(GameContext context, Player player, SpellDesc desc, Entity source, Entity target) { } }
- In the Project panel, navigate to the
-
Author the spell.
- The
player
variable corresponds to the player who's currently invoking the spell. Thesource
is, in this case, the card being played, but is generally the origin of the effect. Thetarget
isnull
in this case, because the player did not choose a target, but it is typically the player's chosen target. - To reuse an existing spell effect, like a summon, create a
new SpellDesc(SummonSpell.class)
, then case it usingSpellUtils.castChildSpell
. The name of the argument tonew SpellDesc
will correspond to the"class":
items you find in the existing cards. -
We want something of the form:
{ "class": "SummonSpell", "card": /*the most common card*/ }
To do this, you will start with a
SpellDesc
andput
arguments into it:SpellDesc summonSpell = new SpellDesc(SummonSpell.class); summonSpell.put(SpellArg.CARD, /* the most common card */);
Observe that the key
"target"
that normally appears in the JSON corresponds to an enum valueSpellArg.TARGET
in theSpellDesc
. You can find correspondingSpellArgs
by looking for theUPPER_CASE
formatted version of JSON keys. Check your work using double Shift to find theSpellArg
. -
Iterate through the player's deck to find the minion card with the most copies, then summon it:
Map<String, Integer> countOfCard = new HashMap<>(); for (int i = 0; i < player.getDeck().size(); i++) { Card card = player.getDeck().get(i); if (card.getCardType() != CardType.MINION) { continue; } int newCount = countOfCard.getOrDefault(card.getCardId(), 1); countOfCard.put(card.getCardId(), newCount); } // Find the highest count card int maxCount = Integer.MIN_VALUE; List<String> maxCardIds = new ArrayList<>(); for (String cardId : countOfCard.keySet()) { int count = countOfCard.get(cardId); if (count > maxCount) { maxCount = count; maxCardIds.clear(); maxCardIds.add(cardId); } else if (count == maxCount) { maxCardIds.add(cardId); } } SpellDesc summonSpell = new SpellDesc(SummonSpell.class); String randomCardId = context.getLogic().removeRandom(maxCardIds); summonSpell.put(SpellArg.CARD, randomCardId); SpellUtils.castChildSpell(context, player, summonSpell, source, target);
-
There are several alternative ways to author this spell and make it better. An important revision is to set the spell to extend a
SummonSpell
instead of aSpell
, so that this effect interacts with other effects that specifically deal with summoning. Then, we'll usesuper.onCast
instead ofSpellUtils.castChildSpell
to call the original effect. We also need to deal with the fact that the spell might not find any minions:public class SummonMinionWithMostCopiesInDeckSpell extends SummonSpell { @Override @Suspendable protected void onCast(GameContext context, Player player, SpellDesc desc, Entity source, Entity target) { Map<String, Integer> countOfCard = new HashMap<>(); for (int i = 0; i < player.getDeck().size(); i++) { Card card = player.getDeck().get(i); if (card.getCardType() != CardType.MINION) { continue; } int newCount = countOfCard.getOrDefault(card.getCardId(), 1); countOfCard.put(card.getCardId(), newCount); } // Find the highest count card int maxCount = Integer.MIN_VALUE; List<String> maxCardIds = new ArrayList<>(); for (String cardId : countOfCard.keySet()) { int count = countOfCard.get(cardId); if (count > maxCount) { maxCount = count; maxCardIds.clear(); maxCardIds.add(cardId); } else if (count == maxCount) { maxCardIds.add(cardId); } } if (maxCardIds.isEmpty()) { return; } SpellDesc summonSpell = new SpellDesc(SummonSpell.class); String randomCardId = context.getLogic().removeRandom(maxCardIds); summonSpell.put(SpellArg.CARD, randomCardId); super.onCast(context, player, summonSpell, source, target); } }
- The
- Write a test for your card using the examples in this document.
4.4 Writing a New Bot
- For an example of an existing bot, navigate to
GameStateValueBehaviour
by searching for it using the double Shift search. - Create a new intelligent bot by navigating to
IntelligentBehaviour
. - Place your cursor on the class name in the editor, hit Alt+Enter, and choose Implement abstract class.
- Name your bot along the pattern of "TechnologyBehaviour". For example, if you use neural networks as the underlying technology, call it
NeuralNetworkBehaviour
. - Implement the methods.
-
Set the bot used by the server to your new bot:
- Navigate to the
Bots
class in thenet
package. - Observe there is a static field,
BEHAVIOUR
. Observe it is a supplier, a zero-arg function that returns a new instance of aBehaviour
. - Change the supplier to provide an instance of your behaviour. For example, if your behaviour's class is
NeuralNetworkBehaviour
, change it toAtomicReference<Supplier<? extends Behaviour>> BEHAVIOUR = new AtomicReference<>(NeuralNetworkBehaviour::new)
.
- Navigate to the
5. Testing
Use these procedures to test your code either with coded tests or by interacting directly with the server.
5.1 Running Test Code
- To test a card, navigate to
CustomTests.java
and observe the pattern for testing cards. This involves learning a lot of Java. You can search for a card's test by hitting Shift twice and writingtest
followed by the card name. For example, to find Abholos's test, searchtestAbholos
. - Click the play button in the editor's left gutter to run the test.
- Commonly, you will have syntax errors in your JSON files. These errors are printed in the test results in the window at the bottom of IntelliJ. They are difficult to interpret.
You can run all game tests by executing ./gradlew.bat game:test
inside ConEmu on Windows. If the engine has an issue parsing your card, you'll see an error in CardValidationTests
with your card name specified. Other errors may occur due to differences in how projects run on Windows versus macOS; check the messages carefully for errors about your cards.
5.2 Understanding Traces
When you run game:test
, changes that cause exceptions in testRandomMassPlay
(a fuzzer) will create files in the game
directory, like game/masstest-trace-2019-06-14T20_21_02_86166.json
.
Use these to help you debug rare interactions or errors you didn't test in your cards.
-
Configure IntelliJ to break on useful exceptions.
- Navigate to Run > View Breakpoints.
- Click the plus icon in the left list and choose Java Exception Breakpoints.
- Write
java.lang.RuntimeException
and hit OK. -
In the right pane:
- Check Suspend, and choose All.
- Check Condition, and set it to
!(this instanceof CancellationException)
. - Check Class filters, and set it to
com.hiddenswitch.* net.demilich.*
. - Under Notifications, check Caught exception and Uncaught Exception.
- Click Done.
- Drag and drop the
.json
trace files intogame/src/test/resources/traces
. - Navigate to
testTraces
by hitting shift twice and searching for it. - Click the play button in the left gutter of the editor, and then choose Debug (the bug icon).
- Observe you will "break" on the exception that caused your test to fail. Look carefully for a
source
variable in the callstack of the Debug pane at the bottom, which you can navigate by clicking further down in the Stack panel. Examine thesource
, which is typically an in-game reference to the card whose effect is causing the issue. - Fix the issue.
- Run the
testTraces
method again, which will exactly reproduce the issue. If the test now passes, you have fixed the issue successfully. - Try running
testRandomMassPlay
by navigating to it with double Shift or by using./gradlew.bat game:test
, and see if it passes now.
5.3 Connecting to a Local Server
- Disable your firewalls.
- Open ConEmu or create a new tab using the green plus icon button.
-
Start the MongoDB database.
- Create a directory to store the data in using the following command:
New-Item -ItemType Directory -Force -Path .mongo
. Observe it is a little verbose, but this ensures you create a directory only if it doesn't already exist. It is strongly recommended to use.mongo
as the directory name, because if you do this command inside yourSpellsource-Server
directory, that.mongo
directory will be specially ignored by git when you save your work. - Run the database using the command
mongod --dbpath .mongo --bind_ip_all
.
- Create a directory to store the data in using the following command:
-
Start the server inside the IntelliJ editor.
- Navigate to the
LocalClustered.java
file. - Click the play button in the editor's left gutter to execute it. Be patient.
- Once you observe
main: Broadcaster deployed
, navigate to the next step.
- Navigate to the
-
Start a client to connect to the local server.
- Download and install the Hidden Switch Launcher if you haven't already.
- Launch the game.
- Observe a popup that says, "Connected to local server ..."
- Create an account. Remember, this belongs to your local server instance only.
- Observe you are now playing on your local server. You can visit the Collection screen to navigate to any new cards.
-
When you are done testing, close the applications.
- Close the Spellsource client application.
- Shut down the server by hitting the red Stop button in the IntelliJ interface.
- Shut down the database by closing the tab in ConEmu.
You can improve the performance of starting the server by disabling Windows Defender. You can permanently disable Windows Defender using Defender Control.
6. Contributing Your Work
- In IntelliJ, go to VCS > Commit...
- Check the boxes next to the files that you have added or modified.
- Author a clear commit message.
- Click Commit.
- Go to VCS > Git > Push.
- Leave the defaults and click Push.
- Finally, go to VCS > Git > Create Pull Request, and follow the on screen instructions.