Support negative numbers in ranges

Adds support for using these in the weapons and ability filters:
* "-1", which was previously treated as an parse error (no number before the separator).
* "-3--1"
* "-infinity" as the lower number in the range, provided a different upper number is given.

This treats "-infinity" (with no other number), "-infinity--infinity",
"infinity" (with no other number) and "infinity-infinity" as errors. It seems
unlikely that someone would intend to use a filter that can't match any
reasonable number.

The range "-infinity-infinity" will be parsed successfully. I don't see a use
case for that, but nor do I see a reason to add extra C++ to reject it.
However, it's not added to the schema, as I think it's good for the schema to
give a warning when someone creates a filter which will accept every value
(including accepting the default, so "-infinity-infinity" accepts the unset
value too).

Includes new unit tests for the C++ and the Lua stringx.parse_range functions.
The next commit adds more WML tests, but is kept separate to credit the author.

This started as a change to move common filter functions from unit.cpp to
somewhere that they could be reused for other config-based filters. In the
process a missing feature was found and added, the move is still included in a
single Git commit because the move was required in order to make these
functions accessible to the Boost unit tests.

Two CodeBlocks project files additionally get src/utils/any.hpp added,
which was in one of them but missing from the other two. I noticed because
these are alphabetically at the start of the src/utils file list.

Thanks to @CelticMinstrel for the review comments and Xcode project updates.
This commit is contained in:
Steve Cotton 2023-08-06 13:51:49 +02:00 committed by Steve Cotton
parent 76246291cd
commit cc8dddea6e
35 changed files with 552 additions and 166 deletions

View file

@ -83,6 +83,9 @@ config_cache/test_macrosubstitution
config_cache/test_transaction
config_cache/test_define_loading
config_cache/test_lead_spaces_loading
config_filters/test_int_add_sub_filter
config_filters/test_int_positive_filter
config_filters/test_int_signed_filter
filesystem/test_fs_game_path_reverse_engineering
filesystem/test_fs_base
filesystem/test_fs_enum

View file

@ -0,0 +1,2 @@
### WML Engine
* Add support for filters to match negative values

View file

@ -1,6 +1,6 @@
#define AI_ASPECT_FILTERS
{SIMPLE_KEY turns range_list}
{SIMPLE_KEY turns unsigned_range_list}
{SIMPLE_KEY time_of_day string_list}
#enddef

View file

@ -82,21 +82,21 @@
glob_on_location_id=*
[/not]
[then]
{REQUIRED_KEY x s_range_list}
{REQUIRED_KEY y s_range_list}
{REQUIRED_KEY x s_unsigned_range_list}
{REQUIRED_KEY y s_unsigned_range_list}
[/then]
[else]
{REQUIRED_KEY location_id string_list}
[/else]
[/if]
{REQUIRED_KEYS_LOC_OR_XY enemy string_list s_range_list}
{REQUIRED_KEYS_LOC_OR_XY enemy string_list s_unsigned_range_list}
{DEFAULT_KEY active_side_leader s_bool no}
{DEFAULT_KEY ca_score s_unsigned 300000}
{SIMPLE_KEY healer_x s_range_list}
{SIMPLE_KEY healer_y s_range_list}
{SIMPLE_KEY healer_x s_unsigned_range_list}
{SIMPLE_KEY healer_y s_unsigned_range_list}
{SIMPLE_KEY healer_loc string_list}
{SIMPLE_KEY leadership_x s_range_list}
{SIMPLE_KEY leadership_y s_range_list}
{SIMPLE_KEY leadership_x s_unsigned_range_list}
{SIMPLE_KEY leadership_y s_unsigned_range_list}
{SIMPLE_KEY leadership_loc string_list}
[/case]
[case]
@ -205,7 +205,7 @@
value=messenger_escort
{FILTER_TAG "filter" unit min=1}
{DEPRECATED_KEY id string}
{REQUIRED_KEYS_LOC_OR_XY waypoint string_list s_range_list}
{REQUIRED_KEYS_LOC_OR_XY waypoint string_list s_unsigned_range_list}
{DEFAULT_KEY ca_score s_unsigned 300000}
{DEFAULT_KEY enemy_death_chance s_real 0.67}
{DEFAULT_KEY messenger_death_chance s_bool 0.0}
@ -216,7 +216,7 @@
value=patrol
{FILTER_TAG "filter" unit min=1}
{DEPRECATED_KEY id string}
{REQUIRED_KEYS_LOC_OR_XY waypoint string_list s_range_list}
{REQUIRED_KEYS_LOC_OR_XY waypoint string_list s_unsigned_range_list}
{SIMPLE_KEY attack string_list}
{DEFAULT_KEY attack_range s_int 1}
{DEFAULT_KEY attack_invisible_enemies s_bool no}

View file

@ -268,7 +268,7 @@
max=infinite
{INSERT_TAG}
{FILTER_TAG "filter_side" side {INSERT_TAG}}
{SIMPLE_KEY side s_range_list}
{SIMPLE_KEY side s_unsigned_range_list}
{SIMPLE_KEY income s_int}
{SIMPLE_KEY recruit string_list}
{SIMPLE_KEY team_name string}
@ -340,8 +340,8 @@
max=infinite
super="$filter_unit"
{INSERT_TAG}
{SIMPLE_KEY to_x s_range_list}
{SIMPLE_KEY to_y s_range_list}
{SIMPLE_KEY to_x s_unsigned_range_list}
{SIMPLE_KEY to_y s_unsigned_range_list}
{SIMPLE_KEY to_location string}
{SIMPLE_KEY dir dir_count_list}
{DEFAULT_KEY clear_shroud s_bool no}
@ -407,7 +407,7 @@
max=infinite
super="$filter_location"
{INSERT_TAG}
{SIMPLE_KEY side s_range_list}
{SIMPLE_KEY side s_unsigned_range_list}
{FILTER_TAG "filter_side" side {INSERT_TAG}}
[/tag]
[tag]
@ -535,8 +535,8 @@
[tag]
name="move"
max=infinite
{SIMPLE_KEY x s_range_list}
{SIMPLE_KEY y s_range_list}
{SIMPLE_KEY x s_unsigned_range_list}
{SIMPLE_KEY y s_unsigned_range_list}
{SIMPLE_KEY skip_sighted string} # TODO: Make this an enum type
[/tag]
[tag]
@ -624,7 +624,7 @@
{SIMPLE_KEY male_message s_t_string}
{SIMPLE_KEY female_message s_t_string}
{SIMPLE_KEY wait_description s_t_string}
{SIMPLE_KEY side_for s_range_list}
{SIMPLE_KEY side_for s_unsigned_range_list}
{SIMPLE_KEY image string}
{SIMPLE_KEY mirror s_bool}
{SIMPLE_KEY second_image string}
@ -772,8 +772,8 @@
name="$fake_unit"
max=0
{SIMPLE_KEY type string}
{SIMPLE_KEY x s_range_list}
{SIMPLE_KEY y s_range_list}
{SIMPLE_KEY x s_unsigned_range_list}
{SIMPLE_KEY y s_unsigned_range_list}
{SIMPLE_KEY side s_unsigned}
{SIMPLE_KEY gender gender,subst}
{SIMPLE_KEY variation string}

View file

@ -71,8 +71,8 @@
[tag]
name="fog_override"
max=infinite
{SIMPLE_KEY x range_list}
{SIMPLE_KEY y range_list}
{SIMPLE_KEY x unsigned_range_list}
{SIMPLE_KEY y unsigned_range_list}
[/tag]
[tag]
name="ai"
@ -441,8 +441,8 @@
[tag]
name="label"
max=infinite
{SIMPLE_KEY x s_range_list}
{SIMPLE_KEY y s_range_list}
{SIMPLE_KEY x s_unsigned_range_list}
{SIMPLE_KEY y s_unsigned_range_list}
{SIMPLE_KEY text s_t_string}
{SIMPLE_KEY tooltip s_t_string} # Is this documented?
{SIMPLE_KEY immutable s_bool}
@ -456,8 +456,8 @@
[tag]
name="item"
max="infinite"
{SIMPLE_KEY x range_list}
{SIMPLE_KEY y range_list}
{SIMPLE_KEY x unsigned_range_list}
{SIMPLE_KEY y unsigned_range_list}
{SIMPLE_KEY image string}
{SIMPLE_KEY halo string}
{SIMPLE_KEY team_name string}
@ -481,8 +481,8 @@
[tag]
name="time_area"
max=infinite
{SIMPLE_KEY x s_range_list}
{SIMPLE_KEY y s_range_list}
{SIMPLE_KEY x s_unsigned_range_list}
{SIMPLE_KEY y s_unsigned_range_list}
{DEFAULT_KEY current_time int 0}
{LINK_TAG "scenario/time"}
[/tag]
@ -523,8 +523,8 @@
{SIMPLE_KEY chance s_unsigned}
{SIMPLE_KEY check_fogged s_bool}
{SIMPLE_KEY check_shrouded s_bool}
{SIMPLE_KEY x s_range_list}
{SIMPLE_KEY y s_range_list}
{SIMPLE_KEY x s_unsigned_range_list}
{SIMPLE_KEY y s_unsigned_range_list}
{DEFAULT_KEY fade_range s_unsigned 3}
{DEFAULT_KEY full_range s_unsigned 14}
{SIMPLE_KEY loop s_int}

View file

@ -2,8 +2,8 @@
#define CHAMBER_TAG_CONTENTS
max=infinite
{SIMPLE_KEY id string}
{SIMPLE_KEY x range_list}
{SIMPLE_KEY y range_list}
{SIMPLE_KEY x unsigned_range_list}
{SIMPLE_KEY y unsigned_range_list}
{SIMPLE_KEY size unsigned}
{SIMPLE_KEY jagged unsigned}
{DEFAULT_KEY chance unsigned 100}

View file

@ -7,9 +7,9 @@
{SIMPLE_KEY overwrite_specials string_list}
{SIMPLE_KEY apply_to string_list}
{SIMPLE_KEY active_on string_list}
{SIMPLE_KEY value s_range_list}
{SIMPLE_KEY add s_range_list}
{SIMPLE_KEY sub s_range_list}
{SIMPLE_KEY value s_int_range_list}
{SIMPLE_KEY add s_int_range_list}
{SIMPLE_KEY sub s_int_range_list}
{SIMPLE_KEY multiply s_real_range_list}
{SIMPLE_KEY divide s_real_range_list}
{SIMPLE_KEY affect_adjacent s_bool}

View file

@ -9,7 +9,7 @@
{SIMPLE_KEY y s_coordinates}
{SIMPLE_KEY area string}
{SIMPLE_KEY include_borders s_bool}
{DEPRECATED_KEY owner_side s_range_list}
{DEPRECATED_KEY owner_side s_unsigned_range_list}
{SIMPLE_KEY find_in string}
{SIMPLE_KEY radius s_int}
{SIMPLE_KEY formula formula}
@ -28,6 +28,6 @@
name="$filter_adjacent_location"
max=0
super="$filter_location"
{SIMPLE_KEY count s_range_list}
{SIMPLE_KEY count s_unsigned_range_list}
{SIMPLE_KEY adjacent s_dir_list}
[/tag]

View file

@ -2,7 +2,7 @@
[tag]
name="$filter_side"
max=0
{SIMPLE_KEY side s_range_list}
{SIMPLE_KEY side s_unsigned_range_list}
{SIMPLE_KEY team_name string}
{SIMPLE_KEY controller string}
{SIMPLE_KEY formula formula}

View file

@ -15,23 +15,23 @@
{SIMPLE_KEY ability string_list}
{SIMPLE_KEY trait string_list}
{SIMPLE_KEY status string_list}
{SIMPLE_KEY side s_range_list}
{SIMPLE_KEY side s_unsigned_range_list}
{DEPRECATED_KEY has_weapon string}
{SIMPLE_KEY canrecruit s_bool}
{SIMPLE_KEY gender gender}
{SIMPLE_KEY role string}
{SIMPLE_KEY level s_range_list}
{SIMPLE_KEY defense s_range_list}
{SIMPLE_KEY movement_cost s_range_list}
{SIMPLE_KEY vision_cost s_range_list}
{SIMPLE_KEY jamming_cost s_range_list}
{SIMPLE_KEY level s_unsigned_range_list}
{SIMPLE_KEY defense s_unsigned_range_list}
{SIMPLE_KEY movement_cost s_unsigned_range_list}
{SIMPLE_KEY vision_cost s_unsigned_range_list}
{SIMPLE_KEY jamming_cost s_unsigned_range_list}
{SIMPLE_KEY x s_coordinates}
{SIMPLE_KEY y s_coordinates}
{SIMPLE_KEY find_in string}
{SIMPLE_KEY formula formula}
{SIMPLE_KEY lua_function string}
{SIMPLE_KEY ai_special string}
{SIMPLE_KEY recall_cost s_range_list}
{SIMPLE_KEY recall_cost s_unsigned_range_list}
{SIMPLE_KEY upkeep upkeep}
{SIMPLE_KEY usage string}
{SIMPLE_KEY alignment alignment}
@ -56,7 +56,7 @@
name="$filter_adjacent"
max=0
super="$filter_unit"
{SIMPLE_KEY count s_range_list}
{SIMPLE_KEY count s_unsigned_range_list}
{SIMPLE_KEY adjacent dir_list}
{SIMPLE_KEY is_enemy s_bool}
[/tag]

View file

@ -12,12 +12,12 @@
{SIMPLE_KEY special_type string_list}
{SIMPLE_KEY special_type_active string_list}
{SIMPLE_KEY formula formula}
{SIMPLE_KEY damage s_range_list}
{SIMPLE_KEY number s_range_list}
{SIMPLE_KEY parry s_range_list}
{SIMPLE_KEY accuracy s_range_list}
{SIMPLE_KEY movement_used s_range_list}
{SIMPLE_KEY attacks_used s_range_list}
{SIMPLE_KEY damage s_unsigned_range_list}
{SIMPLE_KEY number s_unsigned_range_list}
{SIMPLE_KEY parry s_unsigned_range_list}
{SIMPLE_KEY accuracy s_unsigned_range_list}
{SIMPLE_KEY movement_used s_unsigned_range_list}
{SIMPLE_KEY attacks_used s_unsigned_range_list}
{FILTER_BOOLEAN_OPS weapon}
[/tag]

View file

@ -80,8 +80,8 @@
[/type]
[/union]
[/type]
# definition of range_list and s_range_list, before macros are expanded
{LIST_TYPE_COMPLEX range (
# definition of unsigned_range_list and s_unsigned_range_list, before macros are expanded
{LIST_TYPE_COMPLEX unsigned_range (
[element]
value="\d+"
[/element]
@ -92,16 +92,42 @@
value="\d+-infinity"
[/element]
)}
# definition of int_range_list and s_int_range_list, before macros are expanded
{LIST_TYPE_COMPLEX int_range (
[element]
value="(-?)\d+"
[/element]
[element]
value="-\d+-(-?)\d+"
[/element]
[element]
# If the first number is positive, the second one has to be too
value="\d+-\d+"
[/element]
[element]
value="-infinity-(-?)\d+"
[/element]
[element]
value="(-?)\d+-infinity"
[/element]
)}
# definition of real_range_list and s_real_range_list, before macros are expanded
{LIST_TYPE_COMPLEX real_range (
[element]
value="\d+(\.\d+)?"
value="(-?)\d+(\.\d+)?"
[/element]
[element]
value="-\d+(\.\d+)?-(-?)\d+(\.\d+)?"
[/element]
[element]
# If the first number is positive, the second one has to be too
value="\d+(\.\d+)?-\d+(\.\d+)?"
[/element]
[element]
value="\d+(\.\d+)?-infinity"
value="-infinity-(-?)\d+(\.\d+)?"
[/element]
[element]
value="(-)?\d+(\.\d+)?-infinity"
[/element]
)}
[type]
@ -369,7 +395,7 @@
name="coordinates"
[union]
[type]
link=range_list
link=unsigned_range_list
[/type]
[type]
value="recall"
@ -390,7 +416,8 @@
[/type]
{SUBST_TYPE coordinate}
{SUBST_TYPE coordinates}
{SUBST_TYPE range_list}
{SUBST_TYPE unsigned_range_list}
{SUBST_TYPE int_range_list}
{SUBST_TYPE real_range_list}
{SUBST_TYPE terrain_code}
{SUBST_TYPE terrain_list}
@ -471,4 +498,4 @@
{DATA_TAG args 0 1 any}
[/tag]
[/tag]
[/wml_schema]
[/wml_schema]

View file

@ -4,8 +4,8 @@
max=0
# PART 1: Filters
{SIMPLE_KEY value range_list}
{SIMPLE_KEY value_second range_list}
{SIMPLE_KEY value unsigned_range_list}
{SIMPLE_KEY value_second unsigned_range_list}
{SIMPLE_KEY terrain_type terrain_list}
{SIMPLE_KEY direction dir_list}
{SIMPLE_KEY frequency int}

View file

@ -1 +0,0 @@
git doesn't allow adding empty directories; delete this once any test is added

View file

@ -0,0 +1,49 @@
# wmllint: no translatables
#####
# API(s) being tested: stringx.parse_range
##
# Actions:
# Directly call this API function, check that it converts strings to the expected numbers.
##
# Expected end state:
# No asserts failed.
#####
{GENERIC_UNIT_TEST "test_parse_range" (
[event]
name = start
[lua]
code = <<
local lo,hi = stringx.parse_range("1.5-2.5", false)
unit_test.assert_equal(lo, 1, "positive int: low number didn't match")
unit_test.assert_equal(hi, 2, "positive int: high number didn't match")
lo,hi = stringx.parse_range("1.5-2.5", true)
unit_test.assert_approx_equal(lo, 1.5, 0.0001, "positive double: low number didn't match")
unit_test.assert_approx_equal(hi, 2.5, 0.0001, "positive double: high number didn't match")
lo,hi = stringx.parse_range("-3.5--2.5", true)
unit_test.assert_approx_equal(lo, -3.5, 0.0001, "negative double: low number didn't match")
unit_test.assert_approx_equal(hi, -2.5, 0.0001, "negative double: high number didn't match")
lo,hi = stringx.parse_range("0-infinity", false)
unit_test.assert_equal(lo, 0, "int to infinity: low number didn't match")
unit_test.assert_greater(hi, 1000000, "int to infinity: high number didn't match")
lo,hi = stringx.parse_range("0-infinity", true)
unit_test.assert_equal(lo, 0, "double to infinity: low number didn't match")
unit_test.assert_greater(hi, 1000000, "double to infinity: high number didn't match")
lo,hi = stringx.parse_range("-infinity--1", false)
unit_test.assert_less(lo, -1000000, "int from infinity: low number didn't match")
unit_test.assert_equal(hi, -1, "int from infinity: high number didn't match")
lo,hi = stringx.parse_range("-infinity--1", true)
unit_test.assert_less(lo, -1000000, "double from infinity: low number didn't match")
unit_test.assert_approx_equal(hi, -1, 0.0001, "double from infinity: high number didn't match")
>>
[/lua]
{SUCCEED}
[/event]
)}

View file

@ -1138,6 +1138,9 @@
<Unit filename="../../src/units/udisplay.hpp" />
<Unit filename="../../src/units/unit.cpp" />
<Unit filename="../../src/units/unit.hpp" />
<Unit filename="../../src/utils/any.hpp" />
<Unit filename="../../src/utils/config_filters.cpp" />
<Unit filename="../../src/utils/config_filters.hpp" />
<Unit filename="../../src/utils/const_clone.hpp" />
<Unit filename="../../src/utils/context_free_grammar_generator.cpp" />
<Unit filename="../../src/utils/context_free_grammar_generator.hpp" />

View file

@ -1199,6 +1199,7 @@
<Unit filename="../../src/tests/test_commandline_options.cpp" />
<Unit filename="../../src/tests/test_config.cpp" />
<Unit filename="../../src/tests/test_config_cache.cpp" />
<Unit filename="../../src/tests/test_config_filters.cpp" />
<Unit filename="../../src/tests/test_filesystem.cpp" />
<Unit filename="../../src/tests/test_formula_core.cpp" />
<Unit filename="../../src/tests/test_formula_function.cpp" />
@ -1278,6 +1279,8 @@
<Unit filename="../../src/units/unit.hpp" />
<Unit filename="../../src/units/unit_alignments.hpp" />
<Unit filename="../../src/utils/any.hpp" />
<Unit filename="../../src/utils/config_filters.cpp" />
<Unit filename="../../src/utils/config_filters.hpp" />
<Unit filename="../../src/utils/const_clone.hpp" />
<Unit filename="../../src/utils/context_free_grammar_generator.cpp" />
<Unit filename="../../src/utils/context_free_grammar_generator.hpp" />

View file

@ -1249,6 +1249,9 @@
<Unit filename="../../src/units/unit.cpp" />
<Unit filename="../../src/units/unit.hpp" />
<Unit filename="../../src/units/unit_alignments.hpp" />
<Unit filename="../../src/utils/any.hpp" />
<Unit filename="../../src/utils/config_filters.cpp" />
<Unit filename="../../src/utils/config_filters.hpp" />
<Unit filename="../../src/utils/const_clone.hpp" />
<Unit filename="../../src/utils/context_free_grammar_generator.cpp" />
<Unit filename="../../src/utils/context_free_grammar_generator.hpp" />

View file

@ -888,6 +888,10 @@
91B622191B76C0A600B00E0F /* libpangoft2-1.0.0.dylib in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = EC5C243018EF07B4001FA499 /* libpangoft2-1.0.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
91B6221A1B76C0A600B00E0F /* libpixman-1.0.dylib in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = B513B2280ED36BFB0006E551 /* libpixman-1.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
91B6221B1B76C0A600B00E0F /* libpng16.16.dylib in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = EC5C243118EF07B4001FA499 /* libpng16.16.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
91BE22982A826AC700864F64 /* test_config_filters.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91BE22972A826AC700864F64 /* test_config_filters.cpp */; };
91BE229E2A826AF600864F64 /* config_filters.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91BE229D2A826AF600864F64 /* config_filters.cpp */; };
91BE22A42A826AFA00864F64 /* config_filters.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 91BE22A32A826AFA00864F64 /* config_filters.hpp */; };
91BE22C62A83BBB700864F64 /* config_filters.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91BE229D2A826AF600864F64 /* config_filters.cpp */; };
91C548C31D8866ED00FE6A7B /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4D2A99514DAED0E00CAFF31 /* CoreFoundation.framework */; };
91C548DE1D886E0A00FE6A7B /* config.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5599AA80EC62181008DD061 /* config.cpp */; };
91C548DF1D886E2100FE6A7B /* log.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B5599A020EC62181008DD061 /* log.cpp */; };
@ -1533,14 +1537,14 @@
000000000000000000000004 /* achievements_dialog.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = achievements_dialog.hpp; sourceTree = "<group>"; };
000000000000000000000009 /* network_download_file.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = network_download_file.cpp; sourceTree = "<group>"; };
000000000000000000000010 /* network_download_file.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = network_download_file.hpp; sourceTree = "<group>"; };
00574699A982AA23F12B39E0 /* edit_pbl_translation.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = edit_pbl_translation.cpp; path = edit_pbl_translation.cpp; sourceTree = "<group>"; };
09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = carryover_show_gold.cpp; path = carryover_show_gold.cpp; sourceTree = "<group>"; };
00574699A982AA23F12B39E0 /* edit_pbl_translation.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = edit_pbl_translation.cpp; sourceTree = "<group>"; };
09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = carryover_show_gold.cpp; sourceTree = "<group>"; };
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
1234567890ABCDEF12345680 /* file_progress.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_progress.cpp; sourceTree = "<group>"; };
1234567890ABCDEF12345681 /* file_progress.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = file_progress.hpp; sourceTree = "<group>"; };
1C58BBDF21822A930078D25A /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
20E644DC98F26C756364EC2C /* choose_addon.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = choose_addon.cpp; path = choose_addon.cpp; sourceTree = "<group>"; };
27764FB68F02032F1C0B6748 /* statistics_record.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = statistics_record.cpp; path = statistics_record.cpp; sourceTree = "<group>"; };
20E644DC98F26C756364EC2C /* choose_addon.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = choose_addon.cpp; sourceTree = "<group>"; };
27764FB68F02032F1C0B6748 /* statistics_record.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = statistics_record.cpp; sourceTree = "<group>"; };
460D897824DC7830000B1ABC /* game.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = game.cpp; path = ../../src/server/wesnothd/game.cpp; sourceTree = SOURCE_ROOT; };
460D897924DC7830000B1ABC /* ban.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ban.hpp; path = ../../src/server/wesnothd/ban.hpp; sourceTree = SOURCE_ROOT; };
460D897A24DC7830000B1ABC /* player_network.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = player_network.hpp; path = ../../src/server/wesnothd/player_network.hpp; sourceTree = SOURCE_ROOT; };
@ -2159,8 +2163,8 @@
62D24F311519987400350848 /* context_manager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = context_manager.cpp; sourceTree = "<group>"; };
62D24F331519994300350848 /* palette_manager.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = palette_manager.hpp; sourceTree = "<group>"; };
62D24F341519995200350848 /* palette_manager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = palette_manager.cpp; sourceTree = "<group>"; };
67044415B63F5888193BD7A6 /* prompt.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = prompt.cpp; path = prompt.cpp; sourceTree = "<group>"; };
6FA542D78393E8FF067775DA /* edit_pbl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = edit_pbl.cpp; path = edit_pbl.cpp; sourceTree = "<group>"; };
67044415B63F5888193BD7A6 /* prompt.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = prompt.cpp; sourceTree = "<group>"; };
6FA542D78393E8FF067775DA /* edit_pbl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = edit_pbl.cpp; sourceTree = "<group>"; };
84234C54BB84519421FD4136 /* general.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = general.cpp; sourceTree = "<group>"; };
8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8D1107320486CEB800E47090 /* The Battle for Wesnoth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "The Battle for Wesnoth.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@ -2309,6 +2313,9 @@
91B621EF1B76BB3200B00E0F /* mapgen_lua_kernel.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = mapgen_lua_kernel.hpp; sourceTree = "<group>"; };
91B621F01B76BB3500B00E0F /* push_check.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = push_check.hpp; sourceTree = "<group>"; };
91B621F11B76BBC500B00E0F /* rect.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = rect.hpp; sourceTree = "<group>"; };
91BE22972A826AC700864F64 /* test_config_filters.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = test_config_filters.cpp; sourceTree = "<group>"; };
91BE229D2A826AF600864F64 /* config_filters.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = config_filters.cpp; sourceTree = "<group>"; };
91BE22A32A826AFA00864F64 /* config_filters.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = config_filters.hpp; sourceTree = "<group>"; };
91BE78371DD8F5DE00528C21 /* catalog.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = catalog.hpp; sourceTree = "<group>"; };
91BE78381DD8F5DE00528C21 /* catalog_metadata.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = catalog_metadata.hpp; sourceTree = "<group>"; };
91BE78391DD8F5DE00528C21 /* default_plural_forms_compiler.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = default_plural_forms_compiler.hpp; sourceTree = "<group>"; };
@ -2356,7 +2363,7 @@
95EB8A51287A02B800B09F95 /* draw_manager.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = draw_manager.hpp; sourceTree = "<group>"; };
95EB8A53287A02EC00B09F95 /* top_level_drawable.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = top_level_drawable.hpp; path = gui/core/top_level_drawable.hpp; sourceTree = "<group>"; };
95EB8A54287A02EC00B09F95 /* top_level_drawable.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = top_level_drawable.cpp; path = gui/core/top_level_drawable.cpp; sourceTree = "<group>"; };
B2CC45FEA71445AE817CAA6B /* edit_pbl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = edit_pbl.hpp; path = edit_pbl.hpp; sourceTree = "<group>"; };
B2CC45FEA71445AE817CAA6B /* edit_pbl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = edit_pbl.hpp; sourceTree = "<group>"; };
B508D191100146E300B12852 /* engine_fai.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = engine_fai.cpp; sourceTree = "<group>"; };
B508D192100146E300B12852 /* engine_fai.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = engine_fai.hpp; sourceTree = "<group>"; };
B513B2270ED36BFB0006E551 /* libcairo.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcairo.2.dylib; path = lib/libcairo.2.dylib; sourceTree = "<group>"; };
@ -2679,9 +2686,9 @@
B5BB6EFD0F93B83500444FBF /* SDLMain.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = SDLMain.nib; path = Resources/SDLMain.nib; sourceTree = "<group>"; };
B5CE46F712A0417D00D665EE /* side_filter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = side_filter.cpp; sourceTree = "<group>"; };
B5CE46F812A0417D00D665EE /* side_filter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = side_filter.hpp; sourceTree = "<group>"; };
C679447D91FD3623CC852FF8 /* edit_pbl_translation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = edit_pbl_translation.hpp; path = edit_pbl_translation.hpp; sourceTree = "<group>"; };
D4594633BF3F8A06D6AE752F /* prompt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = prompt.hpp; path = prompt.hpp; sourceTree = "<group>"; };
D9A141EAAE90E98B6F6171D6 /* choose_addon.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = choose_addon.hpp; path = choose_addon.hpp; sourceTree = "<group>"; };
C679447D91FD3623CC852FF8 /* edit_pbl_translation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = edit_pbl_translation.hpp; sourceTree = "<group>"; };
D4594633BF3F8A06D6AE752F /* prompt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = prompt.hpp; sourceTree = "<group>"; };
D9A141EAAE90E98B6F6171D6 /* choose_addon.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = choose_addon.hpp; sourceTree = "<group>"; };
EC0341DF1ECF46FE000F2E2B /* config_attribute_value.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = config_attribute_value.cpp; sourceTree = "<group>"; };
EC0341E01ECF46FE000F2E2B /* config_attribute_value.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = config_attribute_value.hpp; sourceTree = "<group>"; };
EC0680231EA920A300EEE03B /* random_deterministic.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = random_deterministic.cpp; sourceTree = "<group>"; };
@ -4388,6 +4395,8 @@
91EF6BF01C9E217C00E2A733 /* utils */ = {
isa = PBXGroup;
children = (
91BE229D2A826AF600864F64 /* config_filters.cpp */,
91BE22A32A826AFA00864F64 /* config_filters.hpp */,
91EF6BFC1C9E22E400E2A733 /* const_clone.hpp */,
91C55DA21CC078820040012E /* context_free_grammar_generator.cpp */,
91C55DA31CC078820040012E /* context_free_grammar_generator.hpp */,
@ -4746,6 +4755,7 @@
91E356091CACA6CB00774252 /* test_addons.cpp */,
91E3560A1CACA6CB00774252 /* test_commandline_options.cpp */,
B597C4A80FACD42E00CE81F5 /* test_config_cache.cpp */,
91BE22972A826AC700864F64 /* test_config_filters.cpp */,
91E3560B1CACA6CB00774252 /* test_config.cpp */,
91E3560C1CACA6CB00774252 /* test_filesystem.cpp */,
91DCA68F1C9360610030F8D0 /* test_formula_core.cpp */,
@ -5057,6 +5067,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
91BE22A42A826AFA00864F64 /* config_filters.hpp in Headers */,
BC624F2F923836331DCFD078 /* choose_addon.hpp in Headers */,
E2F24C0CBC863C3DC8A041EF /* edit_pbl.hpp in Headers */,
9B4B41D29C90B05F03DE21B0 /* edit_pbl_translation.hpp in Headers */,
@ -5751,6 +5762,7 @@
B5599AEB0EC62181008DD061 /* id.cpp in Sources */,
B5599AEA0EC62181008DD061 /* map.cpp in Sources */,
6295C3D9150FC9EB0077D8C5 /* unit_palette.cpp in Sources */,
91BE229E2A826AF600864F64 /* config_filters.cpp in Sources */,
B5599AE90EC62181008DD061 /* types.cpp in Sources */,
B5599AF00EC62181008DD061 /* unit.cpp in Sources */,
B553B6BA12189C5900CC8C58 /* utility.cpp in Sources */,
@ -6022,6 +6034,7 @@
46E2D99025022BF5003D99F3 /* lua_widget.cpp in Sources */,
91E356811CACC71A00774252 /* save_index.cpp in Sources */,
91E356821CACC71A00774252 /* saved_game.cpp in Sources */,
91BE22C62A83BBB700864F64 /* config_filters.cpp in Sources */,
46BDBBEB217C6F6200D2820C /* lua_terrainfilter.cpp in Sources */,
46F92E0C2174F6A400602C1C /* formula_debugger.cpp in Sources */,
91E356831CACC71A00774252 /* savegame.cpp in Sources */,
@ -6359,6 +6372,7 @@
91A215711CAD6E7500927AEA /* manager.cpp in Sources */,
46BDBBED217C6F6200D2820C /* lua_terrainmap.cpp in Sources */,
91A215721CAD6E7500927AEA /* mapgen_lua_kernel.cpp in Sources */,
91BE22982A826AC700864F64 /* test_config_filters.cpp in Sources */,
46F92E662174F6A400602C1C /* tree_view.cpp in Sources */,
46F92E9E2174F6A400602C1C /* menu_button.cpp in Sources */,
46F92DAE2174F6A300602C1C /* gamestate_inspector.cpp in Sources */,

View file

@ -7,6 +7,7 @@ tests/test_addons.cpp
tests/test_commandline_options.cpp
tests/test_config.cpp
tests/test_config_cache.cpp
tests/test_config_filters.cpp
tests/test_filesystem.cpp
tests/test_formula_core.cpp
tests/test_formula_function.cpp

View file

@ -393,6 +393,7 @@ units/race.cpp
units/types.cpp
units/udisplay.cpp
units/unit.cpp
utils/config_filters.cpp
utils/context_free_grammar_generator.cpp
utils/irdya_datetime.cpp
utils/markov_generator.cpp

View file

@ -47,7 +47,7 @@ static lg::log_domain log_wml("wml");
namespace game_events {
namespace builtin_conditions {
std::vector<std::pair<int,int>> default_counts = utils::parse_ranges("1-infinity");
std::vector<std::pair<int,int>> default_counts = utils::parse_ranges_unsigned("1-infinity");
bool have_unit(const vconfig& cfg)
{
@ -55,7 +55,7 @@ namespace builtin_conditions {
return false;
}
std::vector<std::pair<int,int>> counts = cfg.has_attribute("count")
? utils::parse_ranges(cfg["count"]) : default_counts;
? utils::parse_ranges_unsigned(cfg["count"]) : default_counts;
int match_count = 0;
const unit_filter ufilt(cfg);
for(const unit &i : resources::gameboard->units()) {
@ -92,7 +92,7 @@ namespace builtin_conditions {
terrain_filter(cfg, resources::filter_con, false).get_locations(res);
std::vector<std::pair<int,int>> counts = cfg.has_attribute("count")
? utils::parse_ranges(cfg["count"]) : default_counts;
? utils::parse_ranges_unsigned(cfg["count"]) : default_counts;
return in_ranges<int>(res.size(), counts);
}

View file

@ -28,6 +28,7 @@
#include <cassert>
#include <array>
#include <limits>
#include <optional>
#include <stdexcept>
#include <boost/algorithm/string.hpp>
@ -822,24 +823,59 @@ std::vector<std::string> quoted_split(const std::string& val, char c, int flags,
return res;
}
namespace
{
/**
* Internal common code for parse_range and parse_range_real.
*
* If str contains two elements and a separator such as "a-b", returns a and b.
* Otherwise, returns the original string and std::nullopt.
*/
std::pair<std::string, std::optional<std::string>> parse_range_internal_separator(const std::string& str)
{
// If turning this into a list with additional options, ensure that "-" (if present) is last. Otherwise a
// range such as "-2..-1" might be incorrectly split as "-2..", "1".
static const auto separator = std::string{"-"};
// Starting from the second character means that it won't interpret the minus
// sign on a negative number as the separator.
// No need to check the string length first, as str.find() already does that.
auto pos = str.find(separator, 1);
auto length = separator.size();
if(pos != std::string::npos && pos + length < str.size()) {
return {str.substr(0, pos), str.substr(pos + length)};
}
return {str, std::nullopt};
}
} // namespace
std::pair<int, int> parse_range(const std::string& str)
{
const std::string::const_iterator dash = std::find(str.begin(), str.end(), '-');
const std::string a(str.begin(), dash);
const std::string b = dash != str.end() ? std::string(dash + 1, str.end()) : a;
std::pair<int,int> res {0,0};
auto [a, b] = parse_range_internal_separator(str);
std::pair<int, int> res{0, 0};
try {
if (b == "infinity") {
res = std::pair(std::stoi(a), std::numeric_limits<int>::max());
if(a == "-infinity" && b) {
// The "&& b" is so that we treat parse_range("-infinity") the same as parse_range("infinity"),
// both of those will report an invalid range.
res.first = std::numeric_limits<int>::min();
} else {
res = std::pair(std::stoi(a), std::stoi(b));
res.first = std::stoi(a);
}
if (res.second < res.first) {
if(!b) {
res.second = res.first;
} else if(*b == "infinity") {
res.second = std::numeric_limits<int>::max();
} else {
res.second = std::stoi(*b);
if(res.second < res.first) {
res.second = res.first;
}
}
} catch(const std::invalid_argument&) {
ERR_GENERAL << "Invalid range: "<< str;
ERR_GENERAL << "Invalid range: " << str;
}
return res;
@ -847,32 +883,42 @@ std::pair<int, int> parse_range(const std::string& str)
std::pair<double, double> parse_range_real(const std::string& str)
{
const std::string::const_iterator dash = std::find(str.begin(), str.end(), '-');
const std::string a(str.begin(), dash);
const std::string b = dash != str.end() ? std::string(dash + 1, str.end()) : a;
std::pair<double,double> res {0,0};
auto [a, b] = parse_range_internal_separator(str);
std::pair<double, double> res{0, 0};
try {
if(b == "infinity") {
res = std::pair(std::stod(a), std::numeric_limits<double>::infinity());
if(a == "-infinity" && b) {
// There's already a static-assert for is_iec559 in random.cpp, so this isn't limiting the architectures
// that Wesnoth can run on.
static_assert(std::numeric_limits<double>::is_iec559,
"Don't know how negative infinity is treated on this architecture");
res.first = -std::numeric_limits<double>::infinity();
} else {
res = std::pair(std::stod(a), std::stod(b));
res.first = std::stod(a);
}
if(res.second < res.first) {
if(!b) {
res.second = res.first;
} else if(*b == "infinity") {
res.second = std::numeric_limits<double>::infinity();
} else {
res.second = std::stod(*b);
if(res.second < res.first) {
res.second = res.first;
}
}
} catch(const std::invalid_argument&) {
ERR_GENERAL << "Invalid range: "<< str;
ERR_GENERAL << "Invalid range: " << str;
}
return res;
}
std::vector<std::pair<int, int>> parse_ranges(const std::string& str)
std::vector<std::pair<int, int>> parse_ranges_unsigned(const std::string& str)
{
std::vector<std::pair<int, int>> to_return;
for(const std::string& r : utils::split(str)) {
to_return.push_back(parse_range(r));
auto to_return = parse_ranges_int(str);
if(std::any_of(to_return.begin(), to_return.end(), [](const std::pair<int, int>& r) { return r.first < 0; })) {
ERR_GENERAL << "Invalid range (expected values to be zero or positive): " << str;
return {};
}
return to_return;
@ -888,6 +934,16 @@ std::vector<std::pair<double, double>> parse_ranges_real(const std::string& str)
return to_return;
}
std::vector<std::pair<int, int>> parse_ranges_int(const std::string& str)
{
std::vector<std::pair<int, int>> to_return;
for(const std::string& r : utils::split(str)) {
to_return.push_back(parse_range(r));
}
return to_return;
}
void ellipsis_truncate(std::string& str, const std::size_t size)
{
const std::size_t prev_size = str.length();

View file

@ -282,10 +282,41 @@ std::string bullet_list(const T& v, std::size_t indent = 4, const std::string& b
*/
std::string indent(const std::string& string, std::size_t indent_size = 4);
/**
* Recognises the following patterns, and returns a {min, max} pair.
*
* * "1" returns {1, 1}
* * "1-3" returns {1, 3}
* * "1-infinity" returns {1, maximum int}
* * "-1" returns {-1, -1}
* * "-3--1" returns {-3, -1}
*
* Note that:
*
* * "3-1" returns {3, 3} and does not log an error
* * "-1--3" returns {-1, -1} and does not log an error
* * Although "-infinity--1", "2-infinity" and "-infinity-infinity" are all supported,
* * ranges that can't match a reasonable number, e.g. "-infinity" or "infinity..infinity", may be treated as errors.
*/
std::pair<int, int> parse_range(const std::string& str);
std::vector<std::pair<int, int>> parse_ranges(const std::string& str);
/**
* Handles a comma-separated list of inputs to parse_range, in a context that does not expect
* negative values. Will return an empty list if any of the ranges have a minimum that's below
* zero.
*/
std::vector<std::pair<int, int>> parse_ranges_unsigned(const std::string& str);
/**
* Handles a comma-separated list of inputs to parse_range.
*/
std::vector<std::pair<int, int>> parse_ranges_int(const std::string& str);
/**
* Recognises similar patterns to parse_range, and returns a {min, max} pair.
*
* For this function, "infinity" results in std::numeric_limits<double>::infinity.
*/
std::pair<double, double> parse_range_real(const std::string& str);
std::vector<std::pair<double, double>> parse_ranges_real(const std::string& str);

View file

@ -73,7 +73,7 @@ std::vector<int> side_filter::get_teams() const
static bool check_side_number(const team &t, const std::string &str)
{
std::vector<std::pair<int,int>> ranges = utils::parse_ranges(str);
std::vector<std::pair<int,int>> ranges = utils::parse_ranges_unsigned(str);
int side_number = t.side();
std::vector<std::pair<int,int>>::const_iterator range, range_end = ranges.end();

View file

@ -249,9 +249,9 @@ bool terrain_filter::match_internal(const map_location& loc, const unit* ref_uni
}
}
}
static std::vector<std::pair<int,int>> default_counts = utils::parse_ranges("1-6");
static std::vector<std::pair<int,int>> default_counts = utils::parse_ranges_unsigned("1-6");
std::vector<std::pair<int,int>> counts = (*i).has_attribute("count")
? utils::parse_ranges((*i)["count"]) : default_counts;
? utils::parse_ranges_unsigned((*i)["count"]) : default_counts;
if(!in_ranges(match_count, counts)) {
return false;
}

View file

@ -0,0 +1,87 @@
/*
Copyright (C) 2023
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-test"
#include <boost/test/unit_test.hpp>
// Work around a Boost<1.67 bug fixed in 1.67: Include test_case.hpp after
// unit_tests.hpp. https://svn.boost.org/trac10/ticket/13387
#include <boost/test/data/test_case.hpp> // for parametrized test
#include "utils/config_filters.hpp"
using namespace utils::config_filters;
BOOST_AUTO_TEST_SUITE(config_filters)
BOOST_AUTO_TEST_CASE(test_int_positive_filter)
{
// These can be used both as the filter and as the input to be filtered,
// but for the unsigned_matches_if_present function only one of them is
// a valid filter.
config minus_3 {"x", -3};
config plus_3 {"x", 3};
BOOST_ASSERT(!unsigned_matches_if_present(minus_3, minus_3, "x"));
BOOST_ASSERT(unsigned_matches_if_present(plus_3, plus_3, "x"));
BOOST_ASSERT(!unsigned_matches_if_present(minus_3, plus_3, "x"));
BOOST_ASSERT(!unsigned_matches_if_present(plus_3, minus_3, "x"));
}
BOOST_AUTO_TEST_CASE(test_int_signed_filter)
{
// These can be used both as the filter and as the input to be filtered.
config minus_3 {"x", -3};
config plus_3 {"x", 3};
config any_negative {"x", "-infinity--1"};
config any_positive {"x", "1-infinity"};
config any_number {"x", "-infinity-infinity"};
BOOST_ASSERT(int_matches_if_present(minus_3, minus_3, "x"));
BOOST_ASSERT(int_matches_if_present(plus_3, plus_3, "x"));
BOOST_ASSERT(!int_matches_if_present(minus_3, plus_3, "x"));
BOOST_ASSERT(!int_matches_if_present(plus_3, minus_3, "x"));
BOOST_ASSERT(int_matches_if_present(any_negative, minus_3, "x"));
BOOST_ASSERT(!int_matches_if_present(any_positive, minus_3, "x"));
BOOST_ASSERT(int_matches_if_present(any_number, minus_3, "x"));
BOOST_ASSERT(!int_matches_if_present(any_negative, plus_3, "x"));
BOOST_ASSERT(int_matches_if_present(any_positive, plus_3, "x"));
BOOST_ASSERT(int_matches_if_present(any_number, plus_3, "x"));
}
BOOST_AUTO_TEST_CASE(test_int_add_sub_filter)
{
config add_minus_3 {"add", -3};
config sub_3 {"sub", 3};
BOOST_ASSERT(int_matches_if_present(add_minus_3, add_minus_3, "add"));
BOOST_ASSERT(int_matches_if_present_or_negative(add_minus_3, add_minus_3, "add", "sub"));
BOOST_ASSERT(int_matches_if_present(add_minus_3, add_minus_3, "some_other_key"));
// A range, and one that only matches positive numbers
config add_2_to_4 {"add", "2-4"};
config sub_2_to_4 {"sub", "2-4"};
BOOST_ASSERT(!int_matches_if_present(add_2_to_4, add_minus_3, "add"));
BOOST_ASSERT(int_matches_if_present(add_2_to_4, add_minus_3, "some_other_key"));
BOOST_ASSERT(!int_matches_if_present(sub_2_to_4, add_minus_3, "sub"));
BOOST_ASSERT(int_matches_if_present_or_negative(sub_2_to_4, add_minus_3, "sub", "add"));
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -389,7 +389,7 @@ bool unit::ability_active(const std::string& ability,const config& cfg,const map
if (i["count"].empty() && count != dirs.size()) {
return false;
}
if (!in_ranges<int>(count, utils::parse_ranges(i["count"].str()))) {
if (!in_ranges<int>(count, utils::parse_ranges_unsigned(i["count"].str()))) {
return false;
}
}
@ -414,7 +414,7 @@ bool unit::ability_active(const std::string& ability,const config& cfg,const map
if (i["count"].empty() && count != dirs.size()) {
return false;
}
if (!in_ranges<int>(count, utils::parse_ranges(i["count"].str()))) {
if (!in_ranges<int>(count, utils::parse_ranges_unsigned(i["count"].str()))) {
return false;
}
}
@ -1690,7 +1690,7 @@ bool attack_type::special_active_impl(
if (i["count"].empty() && count != dirs.size()) {
return false;
}
if (!in_ranges<int>(count, utils::parse_ranges(i["count"].str()))) {
if (!in_ranges<int>(count, utils::parse_ranges_unsigned(i["count"].str()))) {
return false;
}
}
@ -1713,7 +1713,7 @@ bool attack_type::special_active_impl(
if (i["count"].empty() && count != dirs.size()) {
return false;
}
if (!in_ranges<int>(count, utils::parse_ranges(i["count"].str()))) {
if (!in_ranges<int>(count, utils::parse_ranges_unsigned(i["count"].str()))) {
return false;
}
}

View file

@ -123,22 +123,22 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil
if ( !filter_range.empty() && std::find(filter_range.begin(), filter_range.end(), attack.range()) == filter_range.end() )
return false;
if ( !filter_damage.empty() && !in_ranges(attack.damage(), utils::parse_ranges(filter_damage)) )
if ( !filter_damage.empty() && !in_ranges(attack.damage(), utils::parse_ranges_unsigned(filter_damage)) )
return false;
if (!filter_attacks.empty() && !in_ranges(attack.num_attacks(), utils::parse_ranges(filter_attacks)))
if (!filter_attacks.empty() && !in_ranges(attack.num_attacks(), utils::parse_ranges_unsigned(filter_attacks)))
return false;
if (!filter_accuracy.empty() && !in_ranges(attack.accuracy(), utils::parse_ranges(filter_accuracy)))
if (!filter_accuracy.empty() && !in_ranges(attack.accuracy(), utils::parse_ranges_int(filter_accuracy)))
return false;
if (!filter_parry.empty() && !in_ranges(attack.parry(), utils::parse_ranges(filter_parry)))
if (!filter_parry.empty() && !in_ranges(attack.parry(), utils::parse_ranges_int(filter_parry)))
return false;
if (!filter_movement.empty() && !in_ranges(attack.movement_used(), utils::parse_ranges(filter_movement)))
if (!filter_movement.empty() && !in_ranges(attack.movement_used(), utils::parse_ranges_unsigned(filter_movement)))
return false;
if (!filter_attacks_used.empty() && !in_ranges(attack.attacks_used(), utils::parse_ranges(filter_attacks_used)))
if (!filter_attacks_used.empty() && !in_ranges(attack.attacks_used(), utils::parse_ranges_unsigned(filter_attacks_used)))
return false;
if ( !filter_name.empty() && std::find(filter_name.begin(), filter_name.end(), attack.id()) == filter_name.end() )

View file

@ -151,9 +151,9 @@ struct unit_filter_adjacent : public unit_filter_base
++match_count;
}
static std::vector<std::pair<int,int>> default_counts = utils::parse_ranges("1-6");
static std::vector<std::pair<int,int>> default_counts = utils::parse_ranges_unsigned("1-6");
config::attribute_value i_count = cfg_["count"];
return in_ranges(match_count, !i_count.blank() ? utils::parse_ranges(i_count) : default_counts);
return in_ranges(match_count, !i_count.blank() ? utils::parse_ranges_unsigned(i_count) : default_counts);
}
const unit_filter_compound child_;
@ -603,7 +603,7 @@ void unit_filter_compound::fill(vconfig cfg)
);
create_attribute(literal["recall_cost"],
[](const config::attribute_value& c) { return utils::parse_ranges(c.str()); },
[](const config::attribute_value& c) { return utils::parse_ranges_unsigned(c.str()); },
[](const std::vector<std::pair<int,int>>& ranges, const unit_filter_args& args)
{
for(auto cost : ranges) {
@ -616,7 +616,7 @@ void unit_filter_compound::fill(vconfig cfg)
);
create_attribute(literal["level"],
[](const config::attribute_value& c) { return utils::parse_ranges(c.str()); },
[](const config::attribute_value& c) { return utils::parse_ranges_unsigned(c.str()); },
[](const std::vector<std::pair<int,int>>& ranges, const unit_filter_args& args)
{
for(auto lvl : ranges) {
@ -629,7 +629,7 @@ void unit_filter_compound::fill(vconfig cfg)
);
create_attribute(literal["defense"],
[](const config::attribute_value& c) { return utils::parse_ranges(c.str()); },
[](const config::attribute_value& c) { return utils::parse_ranges_unsigned(c.str()); },
[](const std::vector<std::pair<int,int>>& ranges, const unit_filter_args& args)
{
int actual_defense = args.u.defense_modifier(args.context().get_disp_context().map().get_terrain(args.loc));
@ -643,7 +643,7 @@ void unit_filter_compound::fill(vconfig cfg)
);
create_attribute(literal["movement_cost"],
[](const config::attribute_value& c) { return utils::parse_ranges(c.str()); },
[](const config::attribute_value& c) { return utils::parse_ranges_unsigned(c.str()); },
[](const std::vector<std::pair<int,int>>& ranges, const unit_filter_args& args)
{
int actual_cost = args.u.movement_cost(args.context().get_disp_context().map().get_terrain(args.loc));
@ -657,7 +657,7 @@ void unit_filter_compound::fill(vconfig cfg)
);
create_attribute(literal["vision_cost"],
[](const config::attribute_value& c) { return utils::parse_ranges(c.str()); },
[](const config::attribute_value& c) { return utils::parse_ranges_unsigned(c.str()); },
[](const std::vector<std::pair<int,int>>& ranges, const unit_filter_args& args)
{
int actual_cost = args.u.vision_cost(args.context().get_disp_context().map().get_terrain(args.loc));
@ -671,7 +671,7 @@ void unit_filter_compound::fill(vconfig cfg)
);
create_attribute(literal["jamming_cost"],
[](const config::attribute_value& c) { return utils::parse_ranges(c.str()); },
[](const config::attribute_value& c) { return utils::parse_ranges_unsigned(c.str()); },
[](const std::vector<std::pair<int,int>>& ranges, const unit_filter_args& args)
{
int actual_cost = args.u.jamming_cost(args.context().get_disp_context().map().get_terrain(args.loc));

View file

@ -52,6 +52,7 @@
#include "units/id.hpp"
#include "units/map.hpp" // for unit_map, etc
#include "units/types.hpp"
#include "utils/config_filters.hpp"
#include "variable.hpp" // for vconfig, etc
#include <cassert> // for assert
@ -1420,52 +1421,6 @@ void unit::remove_ability_by_id(const std::string& ability)
}
}
static bool bool_matches_if_present(const config& filter, const config& cfg, const std::string& attribute, bool def)
{
if(filter[attribute].empty()) {
return true;
}
return filter[attribute].to_bool() == cfg[attribute].to_bool(def);
}
static bool string_matches_if_present(const config& filter, const config& cfg, const std::string& attribute, const std::string& def)
{
if(filter[attribute].empty()) {
return true;
}
const std::vector<std::string> filter_attribute = utils::split(filter[attribute]);
return ( std::find(filter_attribute.begin(), filter_attribute.end(), cfg[attribute].str(def)) != filter_attribute.end() );
}
static bool int_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
if(cfg[attribute].empty() && (attribute == "add" || attribute == "sub")){
if(attribute == "add"){
return in_ranges<int>(-cfg["sub"].to_int(0), utils::parse_ranges(filter[attribute].str()));
} else if(attribute == "sub"){
return in_ranges<int>(-cfg["add"].to_int(0), utils::parse_ranges(filter[attribute].str()));
} else {
return false;
}
}
return in_ranges<int>(cfg[attribute].to_int(0), utils::parse_ranges(filter[attribute].str()));
}
static bool double_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
return in_ranges<double>(cfg[attribute].to_double(1), utils::parse_ranges_real(filter[attribute].str()));
}
static bool type_value_if_present(const config& filter, const config& cfg)
{
if(filter["type_value"].empty()) {
@ -1490,6 +1445,8 @@ static bool type_value_if_present(const config& filter, const config& cfg)
static bool matches_ability_filter(const config & cfg, const std::string& tag_name, const config & filter)
{
using namespace utils::config_filters;
if(!filter["affect_adjacent"].empty()){
bool adjacent = cfg.has_child("affect_adjacent");
if(filter["affect_adjacent"].to_bool() != adjacent){
@ -1528,10 +1485,10 @@ static bool matches_ability_filter(const config & cfg, const std::string& tag_na
if(!int_matches_if_present(filter, cfg, "value"))
return false;
if(!int_matches_if_present(filter, cfg, "add"))
if(!int_matches_if_present_or_negative(filter, cfg, "add", "sub"))
return false;
if(!int_matches_if_present(filter, cfg, "sub"))
if(!int_matches_if_present_or_negative(filter, cfg, "sub", "add"))
return false;
if(!double_matches_if_present(filter, cfg, "multiply"))

View file

@ -0,0 +1,85 @@
/*
Copyright (C) 2003 - 2023
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include <algorithm>
#include <vector>
#include "utils/config_filters.hpp"
#include "serialization/string_utils.hpp" // for utils::split
#include "utils/math.hpp" // for in_ranges
bool utils::config_filters::bool_matches_if_present(const config& filter, const config& cfg, const std::string& attribute, bool def)
{
if(filter[attribute].empty()) {
return true;
}
return filter[attribute].to_bool() == cfg[attribute].to_bool(def);
}
bool utils::config_filters::string_matches_if_present(
const config& filter, const config& cfg, const std::string& attribute, const std::string& def)
{
if(filter[attribute].empty()) {
return true;
}
const std::vector<std::string> filter_attribute = utils::split(filter[attribute]);
return (
std::find(filter_attribute.begin(), filter_attribute.end(), cfg[attribute].str(def)) != filter_attribute.end());
}
bool utils::config_filters::unsigned_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
return in_ranges<int>(cfg[attribute].to_int(0), utils::parse_ranges_unsigned(filter[attribute].str()));
}
bool utils::config_filters::int_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
return in_ranges<int>(cfg[attribute].to_int(0), utils::parse_ranges_int(filter[attribute].str()));
}
bool utils::config_filters::int_matches_if_present_or_negative(
const config& filter, const config& cfg, const std::string& attribute, const std::string& opposite)
{
if(int_matches_if_present(filter, cfg, attribute)) {
return true;
}
// don't check for !cfg[opposite].empty(), as this assumes a default value of zero (similarly to
// int_matches_if_present)
if(cfg[attribute].empty()) {
return in_ranges<int>(-cfg[opposite].to_int(0), utils::parse_ranges_int(filter[attribute].str()));
}
return false;
}
bool utils::config_filters::double_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
return in_ranges<double>(cfg[attribute].to_double(1), utils::parse_ranges_real(filter[attribute].str()));
}

View file

@ -0,0 +1,64 @@
/*
Copyright (C) 2003 - 2023
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "config.hpp"
/**
* Utility functions for implementing [filter], [filter_ability], [filter_weapon], etc.
*
* For example, a filter of `x=1` puts a requirement on the value of `x` but accepts any value of `y`.
*
* Both `int` and `double` assume a default value of zero, so a filter that accepts zero will match an
* unset value as well as a present value.
*/
namespace utils::config_filters
{
/**
* Checks whether the filter matches the value of @a cfg[@a attribute]. If @a cfg doesn't have that
* attribute, assume that an unset value is equivalent to @a def.
*
* Always returns true if the filter puts no restriction on the value of @a cfg[@a attribute].
*/
bool bool_matches_if_present(const config& filter, const config& cfg, const std::string& attribute, bool def);
bool double_matches_if_present(const config& filter, const config& cfg, const std::string& attribute);
bool int_matches_if_present(const config& filter, const config& cfg, const std::string& attribute);
/**
* Restricts filters to only looking for values that are zero or more.
*
* A filter such as "-1-10" or "-10--1,1-10" is considered invalid and never matches anything, not
* even a positive value that would be accepted by a stricter subset of the filter.
*/
bool unsigned_matches_if_present(const config& filter, const config& cfg, const std::string& attribute);
/**
* Supports filters using "add" and "sub" attributes, for example a filter `add=1` matching a cfg containing either
* `add=1` or `sub=-1`; this assumes that code elsewhere has already checked that cfg contains at most one of those
* keys.
*
* This only checks for the presence of @a attribute in the filter, so the caller should call this function a second
* time, with @a attribute and @a opposite reversed.
*
* The function is named "negative" in case we later want to add a "reciprocal" for the "multiply"/"divide" pair.
*/
bool int_matches_if_present_or_negative(
const config& filter, const config& cfg, const std::string& attribute, const std::string& opposite);
bool string_matches_if_present(
const config& filter, const config& cfg, const std::string& attribute, const std::string& def);
} // namespace utils::config_filters

View file

@ -242,6 +242,7 @@
0 test_wml_actions
0 test_wml_conditionals
0 lua_wml_tagnames
0 test_parse_range
0 test_scoped_array
0 test_scoped_scalar
0 as_text