Search fixes (#9114)

* Remove old parser and added support for |  (or) and negated text.
This commit is contained in:
Eradev
2025-11-08 04:06:30 -05:00
committed by GitHub
parent 113c422478
commit c5ba0f2c21
6 changed files with 340 additions and 298 deletions

182
docs/Advanced-Search.md Normal file
View File

@@ -0,0 +1,182 @@
# Advanced Search
Forge implements many ways to help you find the cards you want in your ever growing collection. One of them uses a [Scryfall-like syntax](https://scryfall.com/docs/syntax) in the collection search bar.
## Additional information
If no operators are passed between tokens, Forge will assume it is joined by `and`. For example, `t:cat t:warrior t:creature` will search for "creatures that are a cat **and** a warrior". Make sure to use `|` or `or` for your queries, as well as parentheses `( )` when needed.
Keywords can be negated by prefixing a minus sign `-`. For example, `t:creature -t:goblin` will search for "creatures that aren't goblins".
If no keywords are used, Forge will search in their name, type and oracle text for the passed values. For exemple, `(cat | warrior)` will search for cards that has `cat` or `warrior` anywhere in their name, type, or oracle text. Not that it is not bounrd, so it will also match on "catastrophe". This type of search can be negated too. For exemple, `lightning -bolt` will search for card with "lightning and not bolt in their name, types, or oracle text", or `(t:cat | t:warrior) -(orc | angel | phyrexian)` will search for "cat or warrior cards that don't have orc, angel, or phyrexian in their name, types, or oracle text.
## Implemented keywords
### Colors
#### Keyword(s): `color`, `c`
You can find cards that are a certain color using the `c:` or `color:` keyword. Both keywords accepts full color names like blue or the abbreviated color letters `w`, `u`, `r`, `b` and `g`.
You can use many nicknames for color sets: all guild names (e.g. `azorius`), all shard names (e.g. `bant`), all college names (e.g., `quandrix`), all wedge names (e.g. `abzan`), and the four-color nicknames `chaos`, `aggression`, `altruism`, `growth`, `artifice` are supported.
Use `c` or `colorless` to match colorless cards, and `m`, `multi`, or `multicolor` to match multicolor cards.
You can use comparison expressions (`>`, `<`, `>=`, `<=`, and `!=`) to check against ranges of colors.
*Exemples:*
`c:rg` - Cards that are at least red and green
`c!gruul` - Cards that exclusively red and green
`color>=uw -c:red` - Cards that are at least white and blue, but not red
### Card Types
#### Keyword(s): `type:`, `t:`
Find cards of a certain card type with the `t:` or `type:` keywords. You can search for any supertype, card type, or subtype.
Using only partial words is allowed.
*Exemples:*
`t:merfolk t:legend` - Legendary merfolk cards
`t:goblin -t:creature` - Goblin cards that aren't creatures
### Card Text
#### Keyword(s): `oracle:`, `o:`
Use the `o:` or `oracle:` keywords to find cards that have specific phrases in their text box.
You must put quotes `" "` around text with punctuation or spaces.
*Exemples:*
`o:"enters tapped"` - Cards that enter the battlefield tapped
#### Keyword(s): `keyword:`, `kw:`
You can use `keyword:` or `kw:` to search for cards with a specific keyword ability.
> Note: Known to be buggy. You can search by oracle text instead.
*Exemples:*
`kw:flying -t:creature` - Noncreatures that have the flying keyword
#### Keyword(s): `name:`
You can find cards with certain words in their name using `name`.
Supports `!` (exact search), `!=` (doesn't contain), and `:` or `=` (contains).
*Exemples:*
`name!Fire` - The card Fire
`name:Phyrexian`- Cards that contain Phyrexian in their name
#### Keyword(s): `is:vanilla`
Find vanilla cratures (Creatures with no abilities).
### Mana Costs
#### Keyword(s): `manavalue`, `mv`, `cmc`
You can find cards of a specific mana value with `manavalue`, `mv`, or `cmc`, comparing with a numeric expression (>, <, =, >=, <=, and !=).
*Exemples:*
`c:u mv=5` - Blue cards with mana value 5
### Power, Toughness, and Loyalty
#### Keyword(s): `power`, `pow`
You can use numeric expressions (>, <, =, >=, <=, and !=) to find cards with certain power using `power` or `pow`.
*Exemples:*
`pow>=8` - Cards with 8 or more power
`pow>tou c:w t:creature` - White creatures that are top-heavy
#### Keyword(s): `toughness`, `tou`
You can use numeric expressions (>, <, =, >=, <=, and !=) to find cards with certain toughness using `toughness` or `tou`.
*Exemples:*
`tou<=4` - Cards with 4 or less thoughness
#### Keyword(s): `loyalty`, `loy`
You can use numeric expressions (`>`, `<`, `=`, `>=`, `<=`, and `!=`) to find cards with certain starting loyalty using `loyalty` or `loy`.
*Exemples:*
`t:planeswalker loy=3` - Planeswalkers that start at 3 loyalty
### Sets (Editions)
#### Keyword(s): `set:`, `s:`, `edition:`, `e`
Use `s:`, `e:`, `set:`, or `edition:` to find cards using their Magic set code.
Examples:
`e:war` - Cards from War of the Spark
#### Keyword(s): `in:` (set)
The `in:` keyword finds cards that appeared in given set code.
Examples:
`in:lea` - Find cards that once appeared in Alpha.
### Rarity
#### Keyword(s): `rarity:`, `r:`
Use `r:` or `rarity:` to find cards by their print rarity. You can search for `land` (`l`) (usually only basic lands), `common` (`c`), `uncommon` (`u`), `rare` (`r`), `mythic` (`m`), and `special` (`s`). You can also use comparison operators like `<` and `>=.`
Examples:
`r:common t:artifact` - Common artifacts
`r>=r`- Cards at rare rarity or above (rares and mythics)
#### Keyword(s): `in:` (rarity)
You can find cards that have been printed in a given rarity using `in:`
Examples:
`in:rare` - Cards that have been printed at rare.
### Multi-faced cards
#### Keyword(s): `is:split`
Find split-faced cards.
#### Keyword(s): `is:flip`
Find flip cards.
#### Keyword(s): `is:transform`
Find cards that transform.
#### Keyword(s): `is:meld`
Find cards that meld.
#### Keyword(s) `is:leveler`
Find cards with Level Up.
### Others
#### Keyword(s): `is:modal`
Find modal cards.
#### Keyword(s): `is:custom`
Find cards from custom sets.
#### Keyword(s) `is:foil`
Find foil cards.
#### Keyword(s) `is:nonfoil`
Find nonfoil cards.

View File

@@ -109,11 +109,11 @@ Each line is as follow: `CollectorNumber Rarity CardName @ArtistName`.
> Note: You can put the cards in the list even if they aren't scripted yet. Forge will skip over them. > Note: You can put the cards in the list even if they aren't scripted yet. Forge will skip over them.
``` ```text
[tokens] [tokens]
b_1_1_bird_flying 1 b_1_1_bird_flying
b_3_3_cat_deathtouch 2 b_3_3_cat_deathtouch
b_5_5_golem_trample 3 b_5_5_golem_trample
``` ```
The **[tokens]** section is optional, and only needed if you want to use specific token images in this set. They should be named using the name of their token script. `b_1_1_bird_flying` means it is a black 1/1 bird with flying. More on that later. The **[tokens]** section is optional, and only needed if you want to use specific token images in this set. They should be named using the name of their token script. `b_1_1_bird_flying` means it is a black 1/1 bird with flying. More on that later.
@@ -122,7 +122,7 @@ If you load the game with just file, you'll be able to see that Master Chef, Une
Let's comment out Master Chef to avoid a name conflict with an existing MTG card: Let's comment out Master Chef to avoid a name conflict with an existing MTG card:
``` ```text
[cards] [cards]
#7 M Master Chef #7 M Master Chef
33 M Golden Touch 33 M Golden Touch
@@ -131,6 +131,8 @@ Let's comment out Master Chef to avoid a name conflict with an existing MTG card
Save your file, and let's move onto another step. Save your file, and let's move onto another step.
> If there is a conflict, you can add something in its name for differenciate it, such as a set tag (ie. `Master Chef (MSEM)`).
## Scripting your first cards ## Scripting your first cards
As mentioned earlier, your custom card rules need to be located inside `%appdata%/Forge/custom/cards`. I recommend creating subfolders for each starting letter (`Forge/custom/cards/a`, `Forge/custom/cards/b`, etc.) to quickly find if a card has a duplicate name. As mentioned earlier, your custom card rules need to be located inside `%appdata%/Forge/custom/cards`. I recommend creating subfolders for each starting letter (`Forge/custom/cards/a`, `Forge/custom/cards/b`, etc.) to quickly find if a card has a duplicate name.
@@ -141,7 +143,8 @@ Now, you might remember than Unearth was an existing MTG card so we do not need
Let's create the following files: Let's create the following files:
avatar_of_basat.txt avatar_of_basat.txt
```
```text
Name:Avatar of Basat Name:Avatar of Basat
ManaCost:R ManaCost:R
Types:Creature Avatar Types:Creature Avatar
@@ -152,7 +155,8 @@ Oracle:Menace\nAvatar of Basat can't block.
``` ```
exhunt.txt exhunt.txt
```
```text
Name:Exeunt Name:Exeunt
ManaCost:B ManaCost:B
Types:Instant Types:Instant
@@ -162,7 +166,8 @@ Oracle:Each player sacrifices a creature.
``` ```
fox_of_the_orange_orchard.txt fox_of_the_orange_orchard.txt
```
```text
Name:Fox of the Orange Orchard Name:Fox of the Orange Orchard
ManaCost:1 W ManaCost:1 W
Types:Creature Fox Spirit Types:Creature Fox Spirit
@@ -171,7 +176,8 @@ Oracle:
``` ```
inked_summoner.txt inked_summoner.txt
```
```text
Name:Inked Summoner Name:Inked Summoner
ManaCost:1 B ManaCost:1 B
Types:Creature Human Warlock Artist Types:Creature Human Warlock Artist
@@ -190,7 +196,7 @@ Oracle:At the beginning of your end step, if you lost 2 or more life this turn,
If you load your game now, you should be able to find these cards you just scripted! You'll also notice that Inked Summoner is only listed as a Human Warlock, missing the Artist subtype. That's because Artist is not a real MTG subtype. You can add custom types directing inside the set definition file by following the sections found inside the `res/lists/TypeLists.txt` file. Duplicates will be ignored. If you load your game now, you should be able to find these cards you just scripted! You'll also notice that Inked Summoner is only listed as a Human Warlock, missing the Artist subtype. That's because Artist is not a real MTG subtype. You can add custom types directing inside the set definition file by following the sections found inside the `res/lists/TypeLists.txt` file. Duplicates will be ignored.
``` ```text
[CreatureTypes] [CreatureTypes]
Artist:Artists Artist:Artists
``` ```
@@ -205,7 +211,8 @@ Let's add the new tokens we need to make Inked Summoner work!
> Just like for card scripting, this tutorial will not teach you about scripting them. > Just like for card scripting, this tutorial will not teach you about scripting them.
b_1_1_bird.flying.txt b_1_1_bird.flying.txt
```
```text
Name:Bird Token Name:Bird Token
ManaCost:no cost ManaCost:no cost
Colors:black Colors:black
@@ -216,7 +223,8 @@ Oracle:Flying
``` ```
b_3_3_cat_deathtouch.txt b_3_3_cat_deathtouch.txt
```
```text
Name:Cat Token Name:Cat Token
ManaCost:no cost ManaCost:no cost
Colors:black Colors:black
@@ -227,7 +235,8 @@ Oracle:Deathtouch
``` ```
b_5_5_golem_trample.txt b_5_5_golem_trample.txt
```
```text
Name:Golem Token Name:Golem Token
ManaCost:no cost ManaCost:no cost
Colors:black Colors:black
@@ -241,9 +250,9 @@ Great! Now Inked Summoner no longer make the game crash! Now let's add some imag
## Adding card and token images ## Adding card and token images
You can find the card images for the MSEM Champions edition [here](https://msem-instigator.herokuapp.com/set/CHAMPIONS). Find the ones you need and save them inside `%appdata%/../Local/Forge/Cache/pics/cards/MSEM_CHAMPIONS` Remember the filename format should be something like `Swamp.full.jpg` if you only have one variant in your edition. If you have multiples, then it should be something like `Fox of the Orange Orchard1.full.jpg`, `Fox of the Orange Orchard2.full.jpg`, etc. You can find the alternate images from [here](https://msem-instigator.herokuapp.com/set/MPS_MSE) if you want. You can find the card images for the MSEM Champions edition [here](https://msem-instigator.herokuapp.com/set/CHAMPIONS). Find the ones you need and save them inside `%appdata%/../Local/Forge/Cache/pics/cards/MSEM_CHAMPIONS` Remember the filename format should be `{cardname}.fullborder.jpg` if you only have one variant in your edition. If you have multiples, then it should be `{cardname}{number}.fullborder.jpg` (ie. `Fox of the Orange Orchard1.fullborder.jpg`, `Fox of the Orange Orchard2.fullborder.jpg`, etc). You can find the alternate images from [here](https://msem-instigator.herokuapp.com/set/MPS_MSE) if you want.
For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/tokens/MSEM_CHAMPIONS`. They should be named the same as their token script so `b_1_1_bird_flying.jpg` and so forth. For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/tokens/MSEM_CHAMPIONS`. They should be named the same as their number + token script so `1_b_1_1_bird_flying.jpg`, `2_b_3_3_cat_deathtouch.jpg`, and so forth.
![b_1_1_bird_flying](https://github.com/user-attachments/assets/531583c1-3985-4744-858a-3a49fd12740a) ![b_1_1_bird_flying](https://github.com/user-attachments/assets/531583c1-3985-4744-858a-3a49fd12740a)
![b_3_3_cat_deathtouch](https://github.com/user-attachments/assets/15a24e62-be43-4c0c-aeac-0ddb38fca97a) ![b_3_3_cat_deathtouch](https://github.com/user-attachments/assets/15a24e62-be43-4c0c-aeac-0ddb38fca97a)
@@ -251,6 +260,6 @@ For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/toke
You can now start your game again, and see that the art loads correctly now. You can now start your game again, and see that the art loads correctly now.
## 🎉 Congratulations! ## 🎉 Congratulations
Youve just added your first custom set in Forge! There's still much more to explore — scripting advanced abilities, custom mechanics, and set structures — but you now have a solid foundation to build from. Youve just added your first custom set in Forge! There's still much more to explore — scripting advanced abilities, custom mechanics, and set structures — but you now have a solid foundation to build from.

View File

@@ -1,31 +1,39 @@
# Downloads # User Guide
* *Snapshots*
* READ THESE NOTES BEFORE DOWNLOADING SNAPSHOTS:
* May contain more bugs, but also bug fixes, definitely gets newest cards faster and helps us test features.
* These are automatically released daily.
* If the snapshot isn't in the location below, it's because its in the middle of uploading a new snapshot - come back later to grab it.
* [_**CLICK HERE FOR DOWNLOAD LINKS - Forge SNAPSHOT Version (DESKTOP/ANDROID)**_](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
* For desktop, grab the installer file that ends in .jar
* For android, grab the android file that ends in .apk
<br />&dash; Watch the screen recording if one of following steps isn't clear for you
https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8 ## Downloads
* *Releases* ### Snapshots
* READ THESE NOTES BEFORE DOWNLOADING RELEASES:
- "Releases" are really intended where "99% cards implemented are working and stable."
- If you are looking for newly spoiled cards as soon as possible, grab the snapshot instead.
- The current release mechanism is failing unexpectedly for Android. So just stick with snapshots for Android users.
* [_**CLICK HERE FOR DOWNLOAD LINKS - RELEASE DESKTOP**_](https://github.com/Card-Forge/forge/releases/latest)
- Grab the installer file that ends in .jar
# System Requirements * Snapshots are automated daily builds of the source code.
* They contain the latest bug fixes, features and cards.
* If the snapshot isn't in the location below, it's because its in the middle of uploading a new snapshot - come back later to grab it.
[***CLICK HERE FOR DOWNLOAD LINKS - Forge SNAPSHOT Version (DESKTOP/ANDROID)***](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
* For desktop, grab the installer file that ends in .jar
* For android, grab the android file that ends in .apk
&dash; Watch the screen recording if one of following steps isn't clear for you
<https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8>
### Releases
* "Releases" are really intended where "99% cards implemented are working and stable".
* **They are NOT bug-free.** They are not updated after they're built, meaning you need to wait for the next release if you encounter a bug, or use the SNAPSHOT version instead.
* If you are looking for newly spoiled cards as soon as possible, grab the SNAPSHOT version instead.
* The current release mechanism is failing unexpectedly for Android. So just stick with snapshots for Android users.
[***CLICK HERE FOR DOWNLOAD LINKS - RELEASE DESKTOP***](https://github.com/Card-Forge/forge/releases/latest)
* Grab the installer file that ends in .jar
## System Requirements
**Forge Requires Java** to run, please make sure you have Java installed on your machine prior to attempting to run. **Forge Requires Java** to run, please make sure you have Java installed on your machine prior to attempting to run.
* **Java 17** is required as minimum version and can be acquired through the Standard Edition Development Kit (JDK) or the OpenJDK. Continued development provides new features in those editions, therefore you need the Java Development Kit to have those newer editions: * **Java 17** is required as minimum version and can be acquired through the Standard Edition Development Kit (JDK) or the OpenJDK. Continued development provides new features in those editions, therefore you need the Java Development Kit to have those newer editions:
- Download - [https://jdk.java.net/](https://jdk.java.net/) * Download - [https://jdk.java.net/](https://jdk.java.net/)
- Source Code - [https://github.com/openjdk/jdk/](https://github.com/openjdk/jdk/) * Source Code - [https://github.com/openjdk/jdk/](https://github.com/openjdk/jdk/)
Most people who have problems setting up Forge, do not have Java setup properly. If you are having trouble, open your terminal/command line and run `java --version`. That number should be 17 or higher. Most people who have problems setting up Forge, do not have Java setup properly. If you are having trouble, open your terminal/command line and run `java --version`. That number should be 17 or higher.
@@ -39,11 +47,12 @@ We have created several scripts that will launch Forge with a greater
allotment of system resources. (We do this by passing `-Xmx1024m` as allotment of system resources. (We do this by passing `-Xmx1024m` as
an argument to the Java VM.) an argument to the Java VM.)
# Install and Run ## Install and Run
_**Download and unpack/install the package to their own new folder!**_ > Warning: Do **NOT** owerwrite an existing installation. Always unpack/install the package in a new folder to avoid problems!
### Install Wizard (jar)
## Install Wizard (jar)
* Run/Double click "**forge-installer**-VERSION.jar" where VERSION is the current release version and click next until the Target Path window appears. If double clicking the .jar file doesn't load the main interface you can run it via terminal/command line ```java -jar FILENAME.jar``` where FILENAME is the name of the installer. * Run/Double click "**forge-installer**-VERSION.jar" where VERSION is the current release version and click next until the Target Path window appears. If double clicking the .jar file doesn't load the main interface you can run it via terminal/command line ```java -jar FILENAME.jar``` where FILENAME is the name of the installer.
* Browse to your preferred install directory and click next until installation starts. * Browse to your preferred install directory and click next until installation starts.
@@ -52,44 +61,49 @@ _**Download and unpack/install the package to their own new folder!**_
* After the installation finishes, close the installer. Run the executable forge|forge-adventure (.exe/.sh/.cmd) * After the installation finishes, close the installer. Run the executable forge|forge-adventure (.exe/.sh/.cmd)
## What if double-clicking doesnt work? ### What if double-clicking doesnt work?
Sometimes double-clicking will open the jar file in a different program. Sometimes double-clicking will open the jar file in a different program.
In Windows, you may need to right-click and open the properties to change the launching program to Java. In Windows, you may need to right-click and open the properties to change the launching program to Java.
This might be different in OSX or Linux systems (file permission related). This might be different in OSX or Linux systems (file permission related).
## Manual Extraction (tar.bz2) ### Manual Extraction (tar.bz2)
* **Desktop Windows**: #### Desktop Windows
* Unpack "forge...*tar.bz2*" with any unpacking/unzipping app (e.g. 7-zip, winrar, etc)
* You'll end up with "forge...*tar*".
* Unpack that ".tar" file once more into its own folder.
* Run Forge app/exe
* **Desktop Linux/Mac**:
* Unpack "forge...*tar.bz2*" with any unpacking app. (Check your package repository, or app store.)
* You'll probably end up with just a folder, and fully extracted.
* If you do end up with a ".tar" file, unpack that file also into its own folder.
* Run Forge script:
* Linux: Run the ".sh" file in a terminal (double clicking might work.)
* MacOS/OSX: Run the ".command" file by double clicking in Finder, or run from the terminal.
* If the command file doesn't appear to do anything, you'll need to [modify the permissions to be executable.](https://support.apple.com/guide/terminal/make-a-file-executable-apdd100908f-06b3-4e63-8a87-32e71241bab4/mac) (This is a temporary bug in the build process.)
* Additionally OSX needs to have a JRE AND a JDK installed because reasons.
* **Android**:
* Sideload/Install "forge...apk"
* Run Forge
## Play Adventure Mode on Desktop * Unpack "forge...*tar.bz2*" with any unpacking/unzipping app (e.g. 7-zip, winrar, etc)
* You'll end up with "forge...*tar*".
* Unpack that ".tar" file once more into its own folder.
* Run Forge app/exe
#### Desktop Linux/Mac
* Unpack "forge...*tar.bz2*" with any unpacking app. (Check your package repository, or app store.)
* You'll probably end up with just a folder, and fully extracted.
* If you do end up with a ".tar" file, unpack that file also into its own folder.
* Run Forge script:
* Linux: Run the ".sh" file in a terminal (double clicking might work.)
* MacOS/OSX: Run the ".command" file by double clicking in Finder, or run from the terminal.
* If the command file doesn't appear to do anything, you'll need to [modify the permissions to be executable.](https://support.apple.com/guide/terminal/make-a-file-executable-apdd100908f-06b3-4e63-8a87-32e71241bab4/mac) (This is a temporary bug in the build process.)
* Additionally OSX needs to have a JRE AND a JDK installed because reasons.
#### Android
* Sideload/Install "forge...apk"
* Run Forge
### Play Adventure Mode on Desktop
* Run the Adventure Mode EXE or Script in the Folder you extracted. * Run the Adventure Mode EXE or Script in the Folder you extracted.
* The game will start with an option for Adventure or Classic Mobile UI. * The game will start with an option for Adventure or Classic Mobile UI.
* Android/Mobile builds are built as the Adventure Mode or Mobile UI and nothing special is needed. * Android/Mobile builds are built as the Adventure Mode or Mobile UI and nothing special is needed.
- If adventure mode option does not show up; * If adventure mode option does not show up;
- check you're up to date with your version. * check you're up to date with your version.
- check in the settings that the "Selector Mode" is set to `Default` * check in the settings that the "Selector Mode" is set to `Default`
# Gameplay ## Gameplay
## Full Control ### Full Control
Right click/long tap on your player avatar:<br /> Right click/long tap on your player avatar:
This feature lets you skip different helpers that streamline gameplay by avoiding somewhat annoying GUI interactions and instead use AI logic to make reasonable decisions for beginners.\nUseful for certain corner cases or if you want to challenge yourself with the Comprehensive Rules:\ne.g. the opposite cost order is needed if activating an animated "Halo Fountain" that's also tapped. This feature lets you skip different helpers that streamline gameplay by avoiding somewhat annoying GUI interactions and instead use AI logic to make reasonable decisions for beginners.\nUseful for certain corner cases or if you want to challenge yourself with the Comprehensive Rules:\ne.g. the opposite cost order is needed if activating an animated "Halo Fountain" that's also tapped.

View File

@@ -1,4 +1,5 @@
### [Forge Wiki Home](Home.md) ### [Forge Wiki Home](Home.md)
- [User Guide](User-Guide.md) - [User Guide](User-Guide.md)
- [FAQ](Frequently-Asked-Questions.md) - [FAQ](Frequently-Asked-Questions.md)
- [SteamDeck/Bazzite](Steam-Deck-and-Bazzite-Install.md) - [SteamDeck/Bazzite](Steam-Deck-and-Bazzite-Install.md)
@@ -6,21 +7,20 @@
- [Network Play](network-play.md) - [Network Play](network-play.md)
- [Network FAQ](Network-FAQ.md) - [Network FAQ](Network-FAQ.md)
- [Network Extra](Networking-Extras.md) - [Network Extra](Networking-Extras.md)
- [Advanced search](Advanced-Search.md)
- [Adventure Mode](Adventure/Adventure-Mode.md) - [Adventure Mode](Adventure/Adventure-Mode.md)
- Gameplay Guide
- Gameplay Guide - [Getting Started](Adventure/Gameplay-Guide.md)
- [Different Planes](Adventure/Different-Planes.md)
- [Getting Started](Adventure/Gameplay-Guide.md) - [Currency](Adventure/Currency.md)
- [Different Planes](Adventure/Different-Planes.md) - [Deck Building Tips](Adventure/Deck-Building-Tips.md)
- [Currency](Adventure/Currency.md) - [Towns & Capitals](Adventure/Towns-&-Capitals.md)
- [Deck Building Tips](Adventure/Deck-Building-Tips.md) - [Dungeons](Adventure/Dungeons.md)
- [Towns & Capitals](Adventure/Towns-&-Capitals.md) - [Equipments and Items](Adventure/Equipments-and-Items.md)
- [Dungeons](Adventure/Dungeons.md) - [Keyboard Shortcuts](Keyboard-Shortcuts.md)
- [Equipments and Items](Adventure/Equipments-and-Items.md) - [Controller Support](Adventure/GAMEPAD.md)
- [Keyboard Shortcuts](Keyboard-Shortcuts.md) - [Console and Cheats](Adventure/Console-and-Cheats.md)
- [Controller Support](Adventure/GAMEPAD.md)
- [Console and Cheats](Adventure/Console-and-Cheats.md)
- [Modding and content creation](Adventure/Modding.md) - [Modding and content creation](Adventure/Modding.md)

View File

@@ -1,173 +0,0 @@
package forge.itemmanager;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.function.Predicate;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.util.IterableUtil;
import forge.util.PredicateString.StringOp;
public class BooleanExpression {
private Stack<Operator> operators = new Stack<>();
private Stack<Predicate<CardRules>> operands = new Stack<>();
private StringTokenizer expression;
private boolean inName, inType, inText, inCost;
public enum Operator {
AND("&", 0), OR("|", 0), NOT("!", 1), OPEN_PAREN("(", 2), CLOSE_PAREN(")", 2), ESCAPE("\\", -1);
private final String token;
private final int precedence;
Operator(final String token, final int precedence) {
this.token = token;
this.precedence = precedence;
}
public String toString() {
return this.token;
}
}
public BooleanExpression(final String expression, final boolean inName, final boolean inType, final boolean inText, final boolean inCost) {
this.expression = new StringTokenizer(expression);
this.inName = inName;
this.inType = inType;
this.inText = inText;
this.inCost = inCost;
}
public Predicate<CardRules> evaluate() {
StringBuilder currentValue = new StringBuilder();
boolean escapeNext = false;
while (expression.hasNext()) {
String token = expression.next();
Operator operator = null;
if (token.equals(Operator.AND.token)) {
operator = Operator.AND;
} else if (token.equals(Operator.OR.token)) {
operator = Operator.OR;
} else if (token.equals(Operator.OPEN_PAREN.token)) {
operator = Operator.OPEN_PAREN;
} else if (token.equals(Operator.CLOSE_PAREN.token)) {
operator = Operator.CLOSE_PAREN;
} else if (token.equals(Operator.NOT.token) && currentValue.toString().trim().isEmpty()) { //Ignore ! operators that aren't the first token in a search term (Don't use '!' in 'Kaboom!')
operator = Operator.NOT;
} else if (token.equals(Operator.ESCAPE.token)) {
escapeNext = true;
continue;
}
if (operator == null) {
currentValue.append(token);
} else {
if (escapeNext) {
escapeNext = false;
currentValue.append(token);
continue;
}
if (!currentValue.toString().trim().isEmpty()) {
operands.push(valueOf(currentValue.toString().trim()));
}
currentValue = new StringBuilder();
if (!operators.isEmpty() && operator.precedence < operators.peek().precedence) {
resolve(true);
} else if (!operators.isEmpty() && operator == Operator.CLOSE_PAREN) {
while (!operators.isEmpty() && operators.peek() != Operator.OPEN_PAREN) {
resolve(true);
}
}
operators.push(operator);
}
}
if (!currentValue.toString().trim().isEmpty()) {
operands.push(valueOf(currentValue.toString().trim()));
}
while (!operators.isEmpty()) {
resolve(true);
}
return operands.get(0);
}
private void resolve(final boolean alwaysPopOperator) {
Predicate<CardRules> right;
Predicate<CardRules> left;
switch (operators.peek()) {
case AND:
operators.pop();
right = operands.pop();
left = operands.pop();
operands.push(left.and(right));
break;
case OR:
operators.pop();
right = operands.pop();
left = operands.pop();
operands.push(left.or(right));
break;
case NOT:
operators.pop();
left = operands.pop();
operands.push(left.negate());
break;
default:
if (alwaysPopOperator) {
operators.pop();
}
break;
}
}
private Predicate<CardRules> valueOf(final String value) {
List<Predicate<CardRules>> predicates = new ArrayList<>();
if (inName) {
predicates.add(CardRulesPredicates.name(StringOp.CONTAINS_IC, value));
}
if (inType) {
predicates.add(CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, value));
}
if (inText) {
predicates.add(CardRulesPredicates.rules(StringOp.CONTAINS_IC, value));
}
if (inCost) {
predicates.add(CardRulesPredicates.cost(StringOp.CONTAINS_IC, value));
}
if (!predicates.isEmpty()) {
return IterableUtil.or(predicates);
}
return x -> true;
}
public static boolean isExpression(final String string) {
return string.contains(Operator.AND.token) || string.contains(Operator.OR.token) || string.trim().startsWith(Operator.NOT.token);
}
}

View File

@@ -85,6 +85,20 @@ public class SFilterUtil {
continue; continue;
} }
if (ch == '|') {
if (current.length() > 0) {
tokens.add(current.toString());
current = new StringBuilder();
}
tokens.add("or");
continue;
}
if (ch == '-' && current.length() == 0) {
tokens.add("not");
continue;
}
if (!inQuotes && (ch == '(' || ch == ')' || ch == ' ')) { if (!inQuotes && (ch == '(' || ch == ')' || ch == ' ')) {
if (current.length() > 0) { if (current.length() > 0) {
tokens.add(current.toString()); tokens.add(current.toString());
@@ -120,11 +134,11 @@ public class SFilterUtil {
if (!prev.equals("(") && if (!prev.equals("(") &&
!prev.equalsIgnoreCase("or") && !prev.equalsIgnoreCase("or") &&
!prev.equalsIgnoreCase("and") && !prev.equalsIgnoreCase("and") &&
!prev.equalsIgnoreCase("not") &&
!current.equals(")") && !current.equals(")") &&
!current.equalsIgnoreCase("or") && !current.equalsIgnoreCase("or") &&
!current.equalsIgnoreCase("and") && !current.equalsIgnoreCase("and")) {
!current.equalsIgnoreCase("not")) {
result.add("and"); result.add("and");
} }
result.add(current); result.add(current);
@@ -155,21 +169,6 @@ public class SFilterUtil {
} }
} }
if (advancedCardRulesPredicates.isEmpty() && BooleanExpression.isExpression(segment)) {
BooleanExpression expression = new BooleanExpression(segment, inName, inType, inText, inCost);
try {
Predicate<CardRules> filter = expression.evaluate();
if (filter != null) {
if(advancedPaperCardPredicates.isEmpty())
return PaperCardPredicates.fromRules(filter);
return IterableUtil.and(advancedPaperCardPredicates).and(PaperCardPredicates.fromRules(filter));
}
}
catch (Exception e) {
e.printStackTrace();
}
}
Predicate<PaperCard> cardFilter = buildRegularTextPredicate(regularTokens, inName, inType, inText, inCost); Predicate<PaperCard> cardFilter = buildRegularTextPredicate(regularTokens, inName, inType, inText, inCost);
if(!advancedPaperCardPredicates.isEmpty()) if(!advancedPaperCardPredicates.isEmpty())
cardFilter = cardFilter.and(IterableUtil.and(advancedPaperCardPredicates)); cardFilter = cardFilter.and(IterableUtil.and(advancedPaperCardPredicates));
@@ -186,30 +185,33 @@ public class SFilterUtil {
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i); char ch = text.charAt(i);
switch (ch) { switch (ch) {
case ' ': case ' ':
if (!inQuotes) { // If not in quotes, end current entry if (!inQuotes) { // If not in quotes, end the current entry
if (entry.length() > 0) { if (entry.length() > 0) {
splitText.add(entry.toString()); splitText.add(entry.toString());
entry = new StringBuilder(); entry = new StringBuilder();
}
continue;
} }
continue; break;
case '"':
inQuotes = !inQuotes;
continue; // Don't append the quotation character itself
case '\\':
if (i < text.length() - 1 && text.charAt(i + 1) == '"') {
ch = '"'; // Allow appending escaped quotation character
i++; // Prevent changing inQuotes for that character
}
break;
case ',':
if (!inQuotes) { // Ignore commas outside quotes
continue;
}
break;
} }
break;
case '"':
inQuotes = !inQuotes;
continue; // Don't append quotation character itself
case '\\':
if (i < text.length() - 1 && text.charAt(i + 1) == '"') {
ch = '"'; // Allow appending escaped quotation character
i++; // Prevent changing inQuotes for that character
}
break;
case ',':
if (!inQuotes) { // Ignore commas outside quotes
continue;
}
break;
}
entry.append(ch); entry.append(ch);
} }
// Android API StringBuilder isEmpty() is unavailable. https://developer.android.com/reference/java/lang/StringBuilder // Android API StringBuilder isEmpty() is unavailable. https://developer.android.com/reference/java/lang/StringBuilder
@@ -228,15 +230,23 @@ public class SFilterUtil {
for (String s : tokens) { for (String s : tokens) {
List<Predicate<CardRules>> subands = new ArrayList<>(); List<Predicate<CardRules>> subands = new ArrayList<>();
if (inType) { subands.add(CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, s)); } StringOp stringOp = StringOp.CONTAINS_IC;
if (inText) { subands.add(CardRulesPredicates.rules(StringOp.CONTAINS_IC, s)); }
if (inCost) { subands.add(CardRulesPredicates.cost(StringOp.CONTAINS_IC, s)); } // Support for exact match
if (s.startsWith("!")) {
s = s.substring(1);
stringOp = StringOp.EQUALS_IC;
}
if (inType) { subands.add(CardRulesPredicates.joinedType(stringOp, s)); }
if (inText) { subands.add(CardRulesPredicates.rules(stringOp, s)); }
if (inCost) { subands.add(CardRulesPredicates.cost(stringOp, s)); }
Predicate<PaperCard> term; Predicate<PaperCard> term;
if (inName && subands.isEmpty()) if (inName && subands.isEmpty())
term = PaperCardPredicates.searchableName(StringOp.CONTAINS_IC, s); term = PaperCardPredicates.searchableName(stringOp, s);
else if (inName) else if (inName)
term = PaperCardPredicates.searchableName(StringOp.CONTAINS_IC, s).or(PaperCardPredicates.fromRules(IterableUtil.or(subands))); term = PaperCardPredicates.searchableName(stringOp, s).or(PaperCardPredicates.fromRules(IterableUtil.or(subands)));
else else
term = PaperCardPredicates.fromRules(IterableUtil.or(subands)); term = PaperCardPredicates.fromRules(IterableUtil.or(subands));