Add basic achievements functionality. (#7237)

* Add basic achievements functionality.

This reads the mainline achievements.cfg and then all the achievements of each installed add-on.

This is intentionally handled separately from other WML loading so that:
a) All achievements and their status are able to be displayed on the main menu right after Wesnoth starts and regardless of which add-ons are active.
b) Add-ons can add additional achievements to other content, whether UMC or mainline. For example, a modification that adds more achievements for mainline campaigns.

Marking something as achieved is handled by the new [set_achieved] tag and whether an achievement has been completed can be checked via [has_achievement].

There is no attempt to prevent people from manually editing which achievements they've accomplished.

NOTE: These are *not* in any way related to Steam achievements!
This commit is contained in:
Pentarctagon 2023-01-21 10:32:45 -06:00 committed by GitHub
parent 8887d9eac2
commit d1465a9eb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1057 additions and 7 deletions

View file

@ -1,5 +1,6 @@
test_fire_event
test_gui2_iterator
test_gui2/modal_dialog_test_achievements_dialog
test_gui2/modal_dialog_test_addon_auth
test_gui2/modal_dialog_test_addon_connect
test_gui2/modal_dialog_test_addon_license_prompt

97
data/achievements.cfg Normal file
View file

@ -0,0 +1,97 @@
#textdomain wesnoth
# NOTE: due to achievements being loaded separately from the rest of the WML, this macro is not available for general use.
#define ACHIEVEMENT ID NAME DESC
#arg HIDDEN
no#endarg
#arg HIDDEN_NAME
_"???"#endarg
#arg HIDDEN_HINT
#endarg
#arg NAME_COMPLETE
#endarg
#arg DESC_COMPLETE
#endarg
#arg ICON
data/core/images/icons/potion_red_small.png#endarg
#arg ICON_COMPLETE
data/core/images/icons/potion_green_small.png#endarg
[achievement]
id={ID}
name={NAME}
name_completed={NAME_COMPLETE}
description={DESC}
description_completed={DESC_COMPLETE}
icon={ICON}
icon_completed={ICON_COMPLETE}
hidden={HIDDEN}
hidden_name={HIDDEN_NAME}
hidden_hint={HIDDEN_HINT}
[/achievement]
#enddef
# some distros (ie: Debian) put campaigns in separate independent packages, so they aren't all guaranteed to exist even though they're part of mainline
#ifhave campaigns/tutorial
{campaigns/tutorial/achievements.cfg}
#endif
#ifhave campaigns/Dead_Water
{campaigns/Dead_Water/achievements.cfg}
#endif
#ifhave campaigns/Delfadors_Memoirs
{campaigns/Delfadors_Memoirs/achievements.cfg}
#endif
#ifhave campaigns/Descent_Into_Darkness
{campaigns/Descent_Into_Darkness/achievements.cfg}
#endif
#ifhave campaigns/Eastern_Invasion
{campaigns/Eastern_Invasion/achievements.cfg}
#endif
#ifhave campaigns/Heir_To_The_Throne
{campaigns/Heir_To_The_Throne/achievements.cfg}
#endif
#ifhave campaigns/Legend_of_Wesmere
{campaigns/Legend_of_Wesmere/achievements.cfg}
#endif
#ifhave campaigns/Liberty
{campaigns/Liberty/achievements.cfg}
#endif
#ifhave campaigns/Northern_Rebirth
{campaigns/Northern_Rebirth/achievements.cfg}
#endif
#ifhave campaigns/Sceptre_of_Fire
{campaigns/Sceptre_of_Fire/achievements.cfg}
#endif
#ifhave campaigns/Secrets_of_the_Ancients
{campaigns/Secrets_of_the_Ancients/achievements.cfg}
#endif
#ifhave campaigns/Son_Of_The_Black_Eye
{campaigns/Son_Of_The_Black_Eye/achievements.cfg}
#endif
#ifhave campaigns/The_Hammer_of_Thursagan
{campaigns/The_Hammer_of_Thursagan/achievements.cfg}
#endif
#ifhave campaigns/The_Rise_Of_Wesnoth
{campaigns/The_Rise_Of_Wesnoth/achievements.cfg}
#endif
#ifhave campaigns/The_South_Guard
{campaigns/The_South_Guard/achievements.cfg}
#endif
#ifhave campaigns/Two_Brothers
{campaigns/Two_Brothers/achievements.cfg}
#endif
#ifhave campaigns/Under_the_Burning_Suns
{campaigns/Under_the_Burning_Suns/achievements.cfg}
#endif
#ifhave campaigns/Winds_of_Fate
{campaigns/Winds_of_Fate/achievements.cfg}
#endif
#ifhave campaigns/World_Conquest
{campaigns/World_Conquest/achievements.cfg}
#endif

View file

@ -0,0 +1,5 @@
[achievement_group]
display_name=_"Descent into Darkness"
content_for=descent_into_darkness
{ACHIEVEMENT "did_rat_eater" _"Rat Eater" _"Have a Ghoul eat way too many rats during Descent into Darkness's A Haunting in Winter." HIDDEN=yes HIDDEN_HINT=_"Eat some rats!"}
[/achievement_group]

View file

@ -1206,6 +1206,11 @@
speaker=Malin Keshar
message= _ "I must be absolutely mad to be spending hours here just feeding rats to this rotten pile of flesh..."
[/message]
[set_achieved]
content_for="Descent into Darkness"
id="did_rat_eater"
[/set_achieved]
[/event]
[event]
name=rat eating2

View file

View file

@ -0,0 +1,5 @@
[achievement_group]
display_name=_"Tutorial"
content_for=tutorial
{ACHIEVEMENT "completed" _"Complete the Tutorial" _"Complete all scenarios of the Tutorial campaign."}
[/achievement_group]

View file

@ -1498,6 +1498,11 @@ Rest-healing is an exception to the rule — if a unit doesnt do anything for
message= _ "You can also refer to the in-game help browser if you ever need to refresh your memory on gameplay mechanics."
[/message]
[set_achieved]
content_for="Tutorial"
id="tutorial_finished"
[/set_achieved]
{CLEAR_VARIABLE low_hp_unit_message,lhpu_msg_i}
{CLEAR_VARIABLE spoke_about_income,spoke_about_orcs_crossing_river}
{CLEAR_VARIABLE undo_option}

View file

@ -57,6 +57,12 @@
key=a
{IF_APPLE_CMD_ELSE_CTRL}
[/hotkey]
[hotkey]
command=achievements
key=a
shift=yes
{IF_APPLE_CMD_ELSE_CTRL}
[/hotkey]
[hotkey]
command=bestenemymoves
key=b

View file

@ -0,0 +1,220 @@
#textdomain wesnoth-lib
###
### Definition of the window for showing achievements and their statuses
###
[window]
id = "achievements_dialog"
description = "Dialog for displaying achievements and their statuses"
[resolution]
definition = "default"
automatic_placement = true
vertical_placement = "center"
horizontal_placement = "center"
maximum_width = 800
[tooltip]
id = "tooltip"
[/tooltip]
[helptip]
id = "tooltip"
[/helptip]
[linked_group]
id = "achievements"
fixed_width = true
fixed_height = true
[/linked_group]
[grid]
[row]
[column]
horizontal_grow = true
[grid]
[row]
[column]
grow_factor = 1
border = "all"
border_size = 5
horizontal_alignment = "left"
[label]
definition = "title"
label = _ "Achievements"
[/label]
[/column]
[column]
horizontal_alignment = "right"
grow_factor = 0
border = "all"
border_size = 5
[label]
definition = "default"
label = _ "Content:"
[/label]
[/column]
[column]
horizontal_alignment = "right"
grow_factor = 0
border = "all"
border_size = 5
[menu_button]
id = "selected_achievements_list"
definition = "default"
[/menu_button]
[/column]
[/row]
[/grid]
[/column]
[/row]
[row]
grow_factor = 1
[column]
grow_factor = 1
border = "all"
border_size = 5
{GUI_FORCE_WIDGET_MINIMUM_SIZE 600 480 (
[listbox]
id = "achievements_list"
vertical_scrollbar_mode = "always"
horizontal_scrollbar_mode = "never"
[list_definition]
[row]
[column]
horizontal_grow = true
[toggle_panel]
id = "panel"
definition = "default"
linked_group = "achievements"
[grid]
[row]
[column]
grow_factor = 0
horizontal_grow = false
vertical_alignment = "center"
horizontal_alignment = "left"
border = "all"
border_size = 5
[drawing]
id = "icon"
definition = "default"
width = 72
height = 72
[draw]
[image]
name = "(text)"
w = "(min(image_original_width, 60))"
h = "(min(image_original_height, 60))"
{GUI_CENTERED_IMAGE}
[/image]
[/draw]
[/drawing]
[/column]
[column]
grow_factor = 1
horizontal_alignment = "left"
[grid]
[row]
[column]
grow_factor = 1
border = "all"
border_size = 5
horizontal_alignment = "left"
[label]
id = "name"
definition = "default_large"
use_markup = true
[/label]
[/column]
[/row]
[row]
[column]
grow_factor = 1
border = "all"
border_size = 5
horizontal_alignment = "left"
[label]
id = "description"
definition = "default_small"
characters_per_line = 70
use_markup = true
[/label]
[/column]
[/row]
[/grid]
[/column]
[/row]
[/grid]
[/toggle_panel]
[/column]
[/row]
[/list_definition]
[/listbox]
)}
[/column]
[/row]
[row]
grow_factor = 0
[column]
border = "all"
border_size = 5
horizontal_alignment = "right"
[button]
id = "ok"
definition = "default"
label = _ "OK"
[/button]
[/column]
[/row]
[/grid]
[/resolution]
[/window]

View file

@ -38,3 +38,7 @@ function wesnoth.wml_conditionals.variable(cfg)
return old_variable(cfg)
end
end
function wesnoth.wml_conditionals.has_achievement(cfg)
return wesnoth.achievements.has(cfg.content_for, cfg.id);
end

View file

@ -1011,3 +1011,12 @@ function wml_actions.remove_trait(cfg)
unit:remove_modifications({id = obj_id}, "trait")
end
end
function wml_actions.set_achievement(cfg)
local achievement = wesnoth.achievements.get(cfg.content_for, cfg.id)
-- don't show the achievement popup for an achievement they already have
if not achievement.achieved then
wesnoth.achievements.set(cfg.content_for, cfg.id)
gui.show_popup(achievement.name_completed, achievement.description_completed, achievement.icon_completed)
end
end

View file

@ -0,0 +1,27 @@
[wml_schema]
{./macros.cfg}
{./types/basic.cfg}
[tag]
name="root"
[tag]
name="achievement_group"
max="infinite"
{REQUIRED_KEY content_for string}
{REQUIRED_KEY display_name t_string}
[tag]
name="achievement"
max="infinite"
{REQUIRED_KEY id string}
{REQUIRED_KEY name t_string}
{SIMPLE_KEY name_completed t_string}
{REQUIRED_KEY description t_string}
{SIMPLE_KEY description_completed t_string}
{REQUIRED_KEY icon string}
{SIMPLE_KEY icon_completed string}
{SIMPLE_KEY hidden bool}
{SIMPLE_KEY hidden_name t_string}
{SIMPLE_KEY hidden_hint t_string}
[/tag]
[/tag]
[/tag]
[/wml_schema]

View file

@ -0,0 +1,36 @@
#####
# API(s) being tested: [has_achievement]
#####
{GENERIC_UNIT_TEST "has_achievement" (
[event]
name = start
[if]
[has_achievement]
content_for=tutorial
id=completed
[/has_achievement]
[then]
{FAIL}
[/then]
[/if]
[set_achievement]
content_for=tutorial
id=completed
[/set_achievement]
[if]
[has_achievement]
content_for=tutorial
id=completed
[/has_achievement]
[then]
{SUCCEED}
[/then]
[else]
{FAIL}
[/else]
[/if]
[/event]
)}

View file

@ -100,6 +100,8 @@
</Unit>
<Unit filename="../../src/about.cpp" />
<Unit filename="../../src/about.hpp" />
<Unit filename="../../src/achievements.cpp" />
<Unit filename="../../src/achievements.hpp" />
<Unit filename="../../src/actions/advancement.cpp" />
<Unit filename="../../src/actions/advancement.hpp" />
<Unit filename="../../src/actions/attack.cpp" />
@ -565,6 +567,8 @@
<Unit filename="../../src/gui/core/window_builder/helper.hpp" />
<Unit filename="../../src/gui/core/window_builder/instance.cpp" />
<Unit filename="../../src/gui/core/window_builder/instance.hpp" />
<Unit filename="../../src/gui/dialogs/achievements_dialog.cpp" />
<Unit filename="../../src/gui/dialogs/achievements_dialog.hpp" />
<Unit filename="../../src/gui/dialogs/addon/addon_auth.cpp" />
<Unit filename="../../src/gui/dialogs/addon/addon_auth.hpp" />
<Unit filename="../../src/gui/dialogs/addon/connect.cpp" />

View file

@ -98,6 +98,8 @@
</Unit>
<Unit filename="../../src/about.cpp" />
<Unit filename="../../src/about.hpp" />
<Unit filename="../../src/achievements.cpp" />
<Unit filename="../../src/achievements.hpp" />
<Unit filename="../../src/actions/advancement.cpp" />
<Unit filename="../../src/actions/advancement.hpp" />
<Unit filename="../../src/actions/attack.cpp" />
@ -560,6 +562,8 @@
<Unit filename="../../src/gui/core/window_builder/helper.hpp" />
<Unit filename="../../src/gui/core/window_builder/instance.cpp" />
<Unit filename="../../src/gui/core/window_builder/instance.hpp" />
<Unit filename="../../src/gui/dialogs/achievements_dialog.cpp" />
<Unit filename="../../src/gui/dialogs/achievements_dialog.hpp" />
<Unit filename="../../src/gui/dialogs/addon/addon_auth.cpp" />
<Unit filename="../../src/gui/dialogs/addon/addon_auth.hpp" />
<Unit filename="../../src/gui/dialogs/addon/connect.cpp" />

View file

@ -8,7 +8,11 @@
/* Begin PBXBuildFile section */
1234567890ABCDEF12345678 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
000000000000000000000005 /* achievements.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000001 /* achievements.cpp */; };
000000000000000000000006 /* achievements_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000003 /* achievements_dialog.cpp */; };
1234567890ABCDEF12345679 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
000000000000000000000007 /* achievements.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000001 /* achievements.cpp */; };
000000000000000000000008 /* achievements_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000003 /* achievements_dialog.cpp */; };
36B146FAA79A55E9F43723B1 /* general.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 84234C54BB84519421FD4136 /* general.cpp */; };
4291489DA38012477DA3BA7C /* general.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 84234C54BB84519421FD4136 /* general.cpp */; };
3C254DF5B7DF196F2041955F /* mp_report.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 58C649488B3014E6F7254B62 /* mp_report.cpp */; };
@ -1498,6 +1502,10 @@
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>"; };
000000000000000000000001 /* achievements.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = achievements.cpp; sourceTree = "<group>"; };
000000000000000000000002 /* achievements.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = achievements.hpp; sourceTree = "<group>"; };
000000000000000000000003 /* achievements_dialog.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = achievements_dialog.cpp; sourceTree = "<group>"; };
000000000000000000000004 /* achievements_dialog.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = achievements_dialog.hpp; sourceTree = "<group>"; };
1C58BBDF21822A930078D25A /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
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; };
@ -3095,6 +3103,8 @@
children = (
B5599AC60EC62181008DD061 /* about.cpp */,
B5599ABC0EC62181008DD061 /* about.hpp */,
000000000000000000000001 /* achievements.cpp */,
000000000000000000000002 /* achievements.hpp */,
620A386115E9364E00A4F513 /* actions */,
B52EE9A81213640400CFBDAB /* addon */,
B597EBCA0FC082AB00CE81F5 /* ai */,
@ -3637,6 +3647,8 @@
46F92C842174F6A300602C1C /* file_dialog.hpp */,
1234567890ABCDEF12345680 /* file_progress.cpp */,
1234567890ABCDEF12345681 /* file_progress.hpp */,
000000000000000000000003 /* achievements_dialog.cpp */,
000000000000000000000004 /* achievements_dialog.hpp */,
46F92C6E2174F6A300602C1C /* folder_create.cpp */,
46F92C8A2174F6A300602C1C /* folder_create.hpp */,
46F92CBB2174F6A300602C1C /* formula_debugger.cpp */,
@ -5514,6 +5526,8 @@
B52EE8D7121359A600CFBDAB /* persist_manager.cpp in Sources */,
46F92DBB2174F6A300602C1C /* file_dialog.cpp in Sources */,
1234567890ABCDEF12345678 /* file_progress.cpp in Sources */,
000000000000000000000005 /* achievements.cpp in Sources */,
000000000000000000000006 /* achievements_dialog.cpp in Sources */,
46F92F0F2174FEC000602C1C /* standard_colors.cpp in Sources */,
46F92E852174F6A400602C1C /* debug.cpp in Sources */,
B52EE8D8121359A600CFBDAB /* persist_var.cpp in Sources */,
@ -6137,6 +6151,8 @@
91E3570A1CACC9B200774252 /* playcampaign.cpp in Sources */,
46F92DBC2174F6A300602C1C /* file_dialog.cpp in Sources */,
1234567890ABCDEF12345679 /* file_progress.cpp in Sources */,
000000000000000000000007 /* achievements.cpp in Sources */,
000000000000000000000008 /* achievements_dialog.cpp in Sources */,
91E3570B1CACC9B200774252 /* singleplayer.cpp in Sources */,
4649B88B20288EEF00827CFB /* surface.cpp in Sources */,
46F92DB02174F6A300602C1C /* campaign_selection.cpp in Sources */,

View file

@ -1,4 +1,5 @@
about.cpp
achievements.cpp
actions/advancement.cpp
actions/attack.cpp
actions/create.cpp
@ -172,6 +173,7 @@ gui/dialogs/addon/install_dependencies.cpp
gui/dialogs/addon/license_prompt.cpp
gui/dialogs/addon/manager.cpp
gui/dialogs/addon/uninstall_list.cpp
gui/dialogs/achievements_dialog.cpp
gui/dialogs/attack_predictions.cpp
gui/dialogs/campaign_difficulty.cpp
gui/dialogs/campaign_selection.cpp

108
src/achievements.cpp Normal file
View file

@ -0,0 +1,108 @@
/*
Copyright (C) 2003 - 2022
by David White <dave@whitevine.net>
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 "achievements.hpp"
#include "filesystem.hpp"
#include "game_config.hpp"
#include "log.hpp"
#include "preferences/general.hpp"
#include "serialization/parser.hpp"
#include "serialization/preprocessor.hpp"
static lg::log_domain log_config("config");
#define ERR_CONFIG LOG_STREAM(err, log_config)
/**
* Reads the mainline achievements.cfg and then all the achievements of each installed add-on.
*
* This is intentionally handled separately from other WML loading so that:
* a) All achievements and their status are able to be displayed on the main menu right after Wesnoth starts and regardless of which add-ons are active.
* b) Add-ons can add additional achievements to other content, whether UMC or mainline. For example, a modification that adds more achievements for mainline campaigns.
*
* NOTE: These are *not* in any way related to Steam achievements!
*/
achievements::achievements()
: achievement_list_()
{
// mainline
try {
config cfg = read_achievements_file(game_config::path + "/data/achievements.cfg");
process_achievements_file(cfg, "Mainline");
} catch(const game::error& e) {
ERR_CONFIG << "Error processing mainline achievements, ignoring: " << e.what();
}
// add-ons
std::vector<std::string> dirs;
filesystem::get_files_in_dir(filesystem::get_addons_dir(), nullptr, &dirs);
for(const std::string& dir : dirs) {
try {
config cfg = read_achievements_file(filesystem::get_addons_dir() + "/" + dir + "/achievements.cfg");
process_achievements_file(cfg, dir);
} catch(const game::error& e) {
ERR_CONFIG << "Error processing add-on " << dir << " achievements, ignoring: " << e.what();
}
}
}
/**
* Reads an achievements.cfg file into a config.
*
* @param path The path to the achievements.cfg file.
* @return The config containing all the achievements.
*/
config achievements::read_achievements_file(const std::string& path)
{
config cfg;
if(filesystem::file_exists(path)) {
filesystem::scoped_istream stream = preprocess_file(path);
read(cfg, *stream);
}
return cfg;
}
/**
* Processes a config object to add new achievements to @a achievement_list_.
*
* @param cfg The config containing additional achievements.
* @param content_source The source of the additional achievements - either mainline or an add-on.
*/
void achievements::process_achievements_file(const config& cfg, const std::string& content_source)
{
for(const config& achgrp : cfg.child_range("achievement_group")) {
if(achgrp["content_for"].str().empty()) {
ERR_CONFIG << content_source + " achievement_group missing content_for attribute:\n" << achgrp.debug();
continue;
}
achievement_list_.emplace_back(achgrp);
}
}
achievement_group::achievement_group(const config& cfg)
: display_name_(cfg["display_name"].t_str())
, content_for_(cfg["content_for"].str())
, achievements_()
{
for(const config& ach : cfg.child_range("achievement")) {
std::string id = ach["id"].str();
if(id.empty()) {
ERR_CONFIG << content_for_ + " achievement missing id attribute:\n" << ach.debug();
} else {
achievements_.emplace_back(ach, preferences::achievement(content_for_, id));
}
}
}

110
src/achievements.hpp Normal file
View file

@ -0,0 +1,110 @@
/*
Copyright (C) 2003 - 2022
by David White <dave@whitevine.net>
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 <map>
#include <string>
#include <vector>
#include "config.hpp"
#include "tstring.hpp"
/**
* Represents a single achievement and its data.
*/
struct achievement
{
/** The ID of the achievement. Must be unique per achievement_group */
std::string id_;
/** The name of the achievement to show on the UI. */
t_string name_;
/** The name of the achievement to show on the UI if the achievement is completed. */
t_string name_completed_;
/** The description of the achievement to show on the UI. */
t_string description_;
/** The name of the achievement to show on the UI if the achievement is completed. */
t_string description_completed_;
/** The icon of the achievement to show on the UI. */
std::string icon_;
/** The icon of the achievement to show on the UI if the achievement is completed. */
std::string icon_completed_;
/** Whether to show the achievement's actual name and description on the UI before it's been completed. */
bool hidden_;
/** The hint to display in place of the description if the achievement is hidden and uncompleted */
t_string hidden_name_;
/** The hint to display in place of the description if the achievement is hidden and uncompleted */
t_string hidden_hint_;
/** Whether the achievement has been completed. */
bool achieved_;
achievement(const config& cfg, bool achieved)
: id_(cfg["id"].str())
, name_(cfg["name"].t_str())
, name_completed_(cfg["name_completed"].t_str())
, description_(cfg["description"].t_str())
, description_completed_(cfg["description_completed"].t_str())
, icon_(cfg["icon"].str()+"~GS()")
, icon_completed_(cfg["icon_completed"].str())
, hidden_(cfg["hidden"].to_bool())
, hidden_name_(cfg["hidden_name"].t_str())
, hidden_hint_(cfg["hidden_hint"].t_str())
, achieved_(achieved)
{
if(name_completed_.empty()) {
name_completed_ = name_;
}
if(description_completed_.empty()) {
description_completed_ = description_;
}
if(icon_completed_.empty()) {
icon_completed_ = icon_;
}
}
};
/**
* A set of achievements tied to a particular content. Achievements can be added to any content from any add-on, even if it's entirely unrelated.
*/
struct achievement_group
{
/** The name of the content to display on the UI. */
t_string display_name_;
/** The internal ID used for this content. */
std::string content_for_;
/** The achievements associated to this content. */
std::vector<achievement> achievements_;
achievement_group(const config& cfg);
};
/**
* This class is responsible for reading all available achievements from mainline's and any add-ons' achievements.cfg files for use in achievements_dialog.
*/
class achievements
{
public:
achievements();
std::vector<achievement_group>& get_list()
{
return achievement_list_;
}
private:
config read_achievements_file(const std::string& path);
void process_achievements_file(const config& cfg, const std::string& content_source);
std::vector<achievement_group> achievement_list_;
};

View file

@ -61,6 +61,7 @@ game_config_manager::game_config_manager(const commandline_options& cmdline_opts
, old_defines_map_()
, paths_manager_()
, cache_(game_config::config_cache::instance())
, achievements_()
{
assert(!singleton);
singleton = this;

View file

@ -15,12 +15,14 @@
#pragma once
#include "achievements.hpp"
#include "commandline_options.hpp"
#include "config.hpp"
#include "config_cache.hpp"
#include "filesystem.hpp"
#include "game_config_view.hpp"
#include "terrain/type_data.hpp"
#include <optional>
class game_classification;
@ -42,7 +44,8 @@ public:
const game_config_view& game_config() const { return game_config_view_; }
const preproc_map& old_defines_map() const { return old_defines_map_; }
const std::shared_ptr<terrain_type_data> & terrain_types() const { return tdata_; }
const std::shared_ptr<terrain_type_data>& terrain_types() const { return tdata_; }
std::vector<achievement_group>& get_achievements() { return achievements_.get_list(); }
bool init_game_config(FORCE_RELOAD_CONFIG force_reload);
void reload_changed_game_config();
@ -84,4 +87,6 @@ private:
game_config::config_cache& cache_;
std::shared_ptr<terrain_type_data> tdata_;
achievements achievements_;
};

View file

@ -43,6 +43,7 @@
#include "pathfind/pathfind.hpp"
#include "persist_var.hpp"
#include "play_controller.hpp"
#include "preferences/general.hpp"
#include "recall_list_manager.hpp"
#include "replay.hpp"
#include "random.hpp"
@ -239,16 +240,16 @@ wml_action::wml_action(const std::string & tag, handler function)
*
* Generated code looks like this:
* \code
* void wml_func_foo(...);
* static void wml_func_foo(...);
* static wml_action wml_action_foo("foo", &wml_func_foo);
* void wml_func_foo(...)
* static void wml_func_foo(...)
* {
* // code for foo
* }
* \endcode
*/
#define WML_HANDLER_FUNCTION(pname, pei, pcfg) \
static void wml_func_##pname(const queued_event &pei, const vconfig &pcfg); \
static void wml_func_##pname(const queued_event& pei, const vconfig& pcfg); \
static wml_action wml_action_##pname(#pname, &wml_func_##pname); \
static void wml_func_##pname(const queued_event& pei, const vconfig& pcfg)

View file

@ -24,6 +24,7 @@
#include "game_board.hpp"
#include "game_data.hpp"
#include "log.hpp"
#include "preferences/general.hpp"
#include "recall_list_manager.hpp"
#include "resources.hpp"
#include "scripting/game_lua_kernel.hpp"

View file

@ -0,0 +1,125 @@
/*
Copyright (C) 2003 - 2022
by David White <dave@whitevine.net>
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-lib"
#include "gui/dialogs/achievements_dialog.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "gui/auxiliary/find_widget.hpp"
#include "gui/widgets/window.hpp"
#include "log.hpp"
namespace gui2::dialogs
{
REGISTER_DIALOG(achievements_dialog)
achievements_dialog::achievements_dialog()
: modal_dialog(window_id())
, achieve_()
, achievements_box_(nullptr)
, content_names_(nullptr)
{
}
void achievements_dialog::pre_show(window& win)
{
std::vector<config> content_list;
content_names_ = &find_widget<menu_button>(&win, "selected_achievements_list", false);
connect_signal_notify_modified(*content_names_, std::bind(&achievements_dialog::set_achievements_content, this));
achievements_box_ = find_widget<listbox>(&win, "achievements_list", false, true);
auto* a = game_config_manager::get();
auto b = a->get_achievements();
for(const auto& list : b) {
// populate all possibilities into the dropdown
content_list.emplace_back("label", list.display_name_);
// only display the achievements for the first dropdown option on first showing the dialog
if(content_list.size() == 1) {
for(const auto& ach : list.achievements_) {
widget_data row;
widget_item item;
item["label"] = !ach.achieved_ ? ach.icon_ : ach.icon_completed_;
row.emplace("icon", item);
if(ach.hidden_ && !ach.achieved_) {
item["label"] = ach.hidden_name_;
} else if(!ach.achieved_) {
item["label"] = ach.name_;
} else {
item["label"] = "<span color='green'>"+ach.name_completed_+"</span>";
}
row.emplace("name", item);
if(ach.hidden_ && !ach.achieved_) {
item["label"] = ach.hidden_hint_;
} else if(!ach.achieved_) {
item["label"] = ach.description_;
} else {
item["label"] = "<span color='green'>"+ach.description_completed_+"</span>";
}
row.emplace("description", item);
achievements_box_->add_row(row);
}
}
}
if(content_list.size() > 0) {
content_names_->set_values(content_list);
}
}
void achievements_dialog::post_show(window&)
{
}
void achievements_dialog::set_achievements_content()
{
achievements_box_->clear();
for(const auto& ach : game_config_manager::get()->get_achievements().at(content_names_->get_value()).achievements_) {
widget_data row;
widget_item item;
item["label"] = !ach.achieved_ ? ach.icon_ : ach.icon_completed_;
row.emplace("icon", item);
if(ach.hidden_ && !ach.achieved_) {
item["label"] = ach.hidden_name_;
} else if(!ach.achieved_) {
item["label"] = ach.name_;
} else {
item["label"] = "<span color='green'>"+ach.name_completed_+"</span>";
}
row.emplace("name", item);
if(ach.hidden_ && !ach.achieved_) {
item["label"] = ach.hidden_hint_;
} else if(!ach.achieved_) {
item["label"] = ach.description_;
} else {
item["label"] = "<span color='green'>"+ach.description_completed_+"</span>";
}
row.emplace("description", item);
achievements_box_->add_row(row);
}
}
} // namespace gui2::dialogs

View file

@ -0,0 +1,59 @@
/*
Copyright (C) 2003 - 2022
by David White <dave@whitevine.net>
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 "achievements.hpp"
#include "gui/dialogs/modal_dialog.hpp"
#include "gui/widgets/listbox.hpp"
#include "gui/widgets/menu_button.hpp"
#pragma once
namespace gui2::dialogs
{
/**
* @ingroup GUIWindowDefinitionWML
*
* This shows a dialog displaying achievements.
*
* Key |Type |Mandatory|Description
* --------------------------|-------------|---------|-----------
* selected_achievements_list|menu_button |yes |Allows selecting achievements by what content they're for.
* name |label |yes |The user displayed name of the achievement.
* description |label |yes |The achievement's longer description.
* icon |image |yes |An icon to display to the left of the achievement.
*/
class achievements_dialog : public modal_dialog
{
public:
DEFINE_SIMPLE_EXECUTE_WRAPPER(achievements_dialog)
achievements_dialog();
private:
achievements achieve_;
listbox* achievements_box_;
menu_button* content_names_;
void set_achievements_content();
virtual const std::string& window_id() const override;
virtual void pre_show(window& window) override;
virtual void post_show(window& window) override;
};
} // namespace gui2::dialogs

View file

@ -26,6 +26,7 @@
#include "gui/auxiliary/find_widget.hpp"
#include "gui/auxiliary/tips.hpp"
#include "gui/core/timer.hpp"
#include "gui/dialogs/achievements_dialog.hpp"
#include "gui/dialogs/core_selection.hpp"
#include "gui/dialogs/debug_clock.hpp"
#include "gui/dialogs/game_version_dialog.hpp"
@ -163,6 +164,9 @@ void title_screen::init_callbacks()
register_hotkey(hotkey::TITLE_SCREEN__RELOAD_WML,
std::bind(&gui2::window::set_retval, std::ref(*this), RELOAD_GAME_DATA, true));
register_hotkey(hotkey::HOTKEY_ACHIEVEMENTS,
std::bind(&title_screen::show_achievements, this));
register_hotkey(hotkey::TITLE_SCREEN__TEST,
std::bind(&title_screen::hotkey_callback_select_tests, this));
@ -440,6 +444,12 @@ void title_screen::hotkey_callback_select_tests()
}
}
void title_screen::show_achievements()
{
achievements_dialog ach;
ach.show();
}
void title_screen::button_callback_multiplayer()
{
while(true) {

View file

@ -108,6 +108,8 @@ private:
void hotkey_callback_select_tests();
void show_achievements();
void button_callback_multiplayer();
void button_callback_cores();

View file

@ -16,6 +16,7 @@
#include "hotkey/command_executor.hpp"
#include "hotkey/hotkey_item.hpp"
#include "gui/dialogs/achievements_dialog.hpp"
#include "gui/dialogs/lua_interpreter.hpp"
#include "gui/dialogs/message.hpp"
#include "gui/dialogs/screenshot_notification.hpp"
@ -388,6 +389,12 @@ bool command_executor::do_execute_command(const hotkey_command& cmd, int /*inde
preferences::toggle_minimap_draw_villages();
recalculate_minimap();
break;
case HOTKEY_ACHIEVEMENTS:
{
gui2::dialogs::achievements_dialog ach;
ach.show();
}
break;
default:
return false;
}

View file

@ -94,6 +94,7 @@ constexpr std::array<hotkey_command_temp, HOTKEY_NULL - 1> master_hotkey_list {{
{ HOTKEY_ZOOM_OUT, "zoomout", N_("Zoom Out"), false, scope_game | scope_editor, HKCAT_GENERAL, "" },
{ HOTKEY_ZOOM_DEFAULT, "zoomdefault", N_("Default Zoom"), false, scope_game | scope_editor, HKCAT_GENERAL, "" },
{ HOTKEY_FULLSCREEN, "fullscreen", N_("Toggle Full Screen"), false, scope_game | scope_editor | scope_main, HKCAT_GENERAL, "" },
{ HOTKEY_ACHIEVEMENTS, "achievements", N_("Achievements"), false, scope_game | scope_main, HKCAT_GENERAL, "" },
{ HOTKEY_SCREENSHOT, "screenshot", N_("Screenshot"), false, scope_game | scope_editor | scope_main, HKCAT_GENERAL, "" },
{ HOTKEY_MAP_SCREENSHOT, "mapscreenshot", N_("Map Screenshot"), false, scope_game | scope_editor, HKCAT_GENERAL, "" },
{ HOTKEY_ACCELERATED, "accelerated", N_("Toggle Accelerated Speed"), false, scope_game, HKCAT_GENERAL, "" },

View file

@ -96,6 +96,7 @@ enum HOTKEY_COMMAND {
HOTKEY_AI_FORMULA,
HOTKEY_CLEAR_MSG,
HOTKEY_LABEL_SETTINGS,
HOTKEY_ACHIEVEMENTS,
// Minimap
HOTKEY_MINIMAP_CODING_TERRAIN, HOTKEY_MINIMAP_CODING_UNIT,

View file

@ -336,6 +336,7 @@ bool play_controller::hotkey_handler::can_execute_command(const hotkey::hotkey_c
case hotkey::HOTKEY_SCROLL_DOWN:
case hotkey::HOTKEY_SCROLL_LEFT:
case hotkey::HOTKEY_SCROLL_RIGHT:
case hotkey::HOTKEY_ACHIEVEMENTS:
return true;
case hotkey::HOTKEY_SURRENDER: {

View file

@ -1026,5 +1026,41 @@ void set_addon_manager_saved_order_direction(sort_order::type value)
set("addon_manager_saved_order_direction", sort_order::get_string(value));
}
bool achievement(const std::string& content_for, const std::string& id)
{
for(config& ach : prefs.child_range("achievements"))
{
if(ach["content_for"].str() == content_for)
{
std::vector<std::string> ids = utils::split(ach["ids"]);
return std::find(ids.begin(), ids.end(), id) != ids.end();
}
}
return false;
}
void set_achievement(const std::string& content_for, const std::string& id)
{
for(config& ach : prefs.child_range("achievements"))
{
// if achievements already exist for this content and the achievement has not already been set, add it
if(ach["content_for"].str() == content_for)
{
std::vector<std::string> ids = utils::split(ach["ids"]);
if(std::find(ids.begin(), ids.end(), id) == ids.end())
{
ach["ids"] = ach["ids"].str() + "," + id;
}
return;
}
}
// else no achievements have been set for this content yet
config ach;
ach["content_for"] = content_for;
ach["ids"] = id;
prefs.add_child("achievements", ach);
}
} // end namespace preferences

View file

@ -277,4 +277,7 @@ namespace preferences {
sort_order::type addon_manager_saved_order_direction();
void set_addon_manager_saved_order_direction(sort_order::type value);
bool achievement(const std::string& content_for, const std::string& id);
void set_achievement(const std::string& content_for, const std::string& id);
} // end namespace preferences

View file

@ -67,6 +67,7 @@
#include "pathfind/pathfind.hpp" // for full_cost_map, plain_route, etc
#include "pathfind/teleport.hpp" // for get_teleport_locations, etc
#include "play_controller.hpp" // for play_controller
#include "preferences/general.hpp"
#include "recall_list_manager.hpp" // for recall_list_manager
#include "replay.hpp" // for get_user_choice, etc
#include "reports.hpp" // for register_generator, etc
@ -3064,6 +3065,102 @@ int game_lua_kernel::intf_play_sound(lua_State *L)
return 0;
}
/**
* Sets an achievement as being completed.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
*/
int game_lua_kernel::intf_set_achievement(lua_State *L)
{
const char *content_for = luaL_checkstring(L, 1);
const char *id = luaL_checkstring(L, 2);
for(achievement_group& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(achievement& achieve : group.achievements_) {
if(achieve.id_ == id) {
// found the achievement - mark it as completed
preferences::set_achievement(content_for, id);
achieve.achieved_ = true;
return 0;
}
}
// achievement not found - existing achievement group but non-existing achievement id
ERR_LUA << "Achievement " << id << " not found for achievement group " << content_for;
return 0;
}
}
// achievement group not found
ERR_LUA << "Achievement group " << content_for << " not found";
return 0;
}
/**
* Returns whether an achievement has been completed.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_has_achievement(lua_State *L)
{
const char *content_for = luaL_checkstring(L, 1);
const char *id = luaL_checkstring(L, 2);
if(resources::controller->is_networked_mp() && synced_context::is_synced()) {
ERR_LUA << "Returning false for whether a player has completed an achievement due to being networked multiplayer.";
lua_pushboolean(L, false);
} else {
lua_pushboolean(L, preferences::achievement(content_for, id));
}
return 1;
}
/**
* Returns information on a single achievement, or no data if the achievement is not found.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
* - Ret 1: WML table returned by the function.
*/
int game_lua_kernel::intf_get_achievement(lua_State *L)
{
const char *content_for = luaL_checkstring(L, 1);
const char *id = luaL_checkstring(L, 2);
config cfg;
for(const auto& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(const auto& achieve : group.achievements_) {
if(achieve.id_ == id) {
// found the achievement - return it as a config
cfg["id"] = achieve.id_;
cfg["name"] = achieve.name_;
cfg["name_completed"] = achieve.name_completed_;
cfg["description"] = achieve.description_;
cfg["description_completed"] = achieve.description_completed_;
cfg["icon"] = achieve.icon_;
cfg["icon_completed"] = achieve.icon_completed_;
cfg["hidden"] = achieve.hidden_;
cfg["hidden_name"] = achieve.hidden_name_;
cfg["hidden_hint"] = achieve.hidden_hint_;
cfg["achieved"] = achieve.achieved_;
luaW_pushconfig(L, cfg);
return 1;
}
}
// return empty config - existing achievement group but non-existing achievement id
ERR_LUA << "Achievement " << id << " not found for achievement group " << content_for;
luaW_pushconfig(L, cfg);
return 1;
}
}
// return empty config - non-existing achievement group
ERR_LUA << "Achievement group " << content_for << " not found";
luaW_pushconfig(L, cfg);
return 1;
}
/**
* Scrolls to given tile.
* - Arg 1: location.
@ -4885,6 +4982,20 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports
lua_setfield(L, -2, "interface");
lua_pop(L, 1);
// Create the achievements module
cmd_log_ << "Adding achievements module...\n";
static luaL_Reg const achievement_callbacks[] {
{ "set", &dispatch<&game_lua_kernel::intf_set_achievement> },
{ "has", &dispatch<&game_lua_kernel::intf_has_achievement> },
{ "get", &dispatch<&game_lua_kernel::intf_get_achievement> },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, achievement_callbacks, 0);
lua_setfield(L, -2, "achievements");
lua_pop(L, 1);
// Create the audio module
cmd_log_ << "Adding audio module...\n";
static luaL_Reg const audio_callbacks[] {

View file

@ -119,6 +119,9 @@ class game_lua_kernel : public lua_kernel_base
int intf_heal_unit(lua_State *L);
int intf_message(lua_State *L);
int intf_play_sound(lua_State *L);
int intf_set_achievement(lua_State *L);
int intf_has_achievement(lua_State *L);
int intf_get_achievement(lua_State *L);
int intf_set_floating_label(lua_State* L, bool spawn);
int intf_remove_floating_label(lua_State* L);
int intf_move_floating_label(lua_State* L);

View file

@ -22,6 +22,7 @@
#include "filesystem.hpp"
#include "formula/debugger.hpp"
#include "game_config.hpp"
#include "game_config_manager.hpp"
#include "game_config_view.hpp"
#include "game_display.hpp"
#include "game_events/manager.hpp"
@ -37,6 +38,7 @@
#include "gui/dialogs/addon/install_dependencies.hpp"
#include "gui/dialogs/addon/license_prompt.hpp"
#include "gui/dialogs/addon/manager.hpp"
#include "gui/dialogs/achievements_dialog.hpp"
#include "gui/dialogs/attack_predictions.hpp"
#include "gui/dialogs/campaign_difficulty.hpp"
#include "gui/dialogs/campaign_selection.hpp"
@ -131,9 +133,12 @@ using namespace gui2::dialogs;
struct test_gui2_fixture {
test_gui2_fixture()
: config_manager()
, dummy_args({"wesnoth", "--noaddons"})
{
/** The main config, which contains the entire WML tree. */
game_config_view game_config_view_ = game_config_view::wrap(main_config);
config_manager.reset(new game_config_manager(dummy_args));
game_config::config_cache& cache = game_config::config_cache::instance();
@ -152,6 +157,8 @@ struct test_gui2_fixture {
}
static config main_config;
static const std::string widgets_file;
std::unique_ptr<game_config_manager> config_manager;
std::vector<std::string> dummy_args;
};
config test_gui2_fixture::main_config;
const std::string test_gui2_fixture::widgets_file = "widgets_tested.log";
@ -589,6 +596,10 @@ BOOST_AUTO_TEST_CASE(modal_dialog_test_wml_message_double)
{
test<wml_message_double>();
}
BOOST_AUTO_TEST_CASE(modal_dialog_test_achievements_dialog)
{
test<achievements_dialog>();
}
BOOST_AUTO_TEST_CASE(modeless_dialog_test_debug_clock)
{
test_popup<debug_clock>();

View file

@ -33,6 +33,7 @@ validate() {
validate_core() { validate "$1" ./wesnoth --validate data/_main.cfg; }
validate_misc() { validate "$1" ./wesnoth --data-dir=. --validate=data/_main.cfg --preprocess-defines="$2"; }
validate_achievements() { validate "Achievements" ./wesnoth --data-dir=. --validate=data/achievements.cfg --use-schema=data/schema/achievements.cfg; }
validate_schema() { validate "$1" ./wesnoth --data-dir=. --validate-schema=data/schema/"$2".cfg; }
validate_campaign() {
@ -75,11 +76,17 @@ validate_schema "Game Config" "game_config" || RET=1
validate_schema "GUI2" "gui" || RET=1
validate_schema "Server Pbl" "pbl" || RET=1
validate_schema "WML Diff" "diff" || RET=1
validate_schema "Achievements schema" "achievements" || RET=1
validate_core "Core" || RET=1
validate_misc "Editor" "EDITOR" || RET=1
validate_misc "Multiplayer" "MULTIPLAYER,MULTIPLAYER_A_NEW_LAND_LOAD" || RET=1
validate_misc "Test" "TEST" || RET=1
validate_achievements || RET=1
validate_misc "Editor" "EDITOR" || RET=1
validate_misc "Multiplayer" "MULTIPLAYER,MULTIPLAYER_A_NEW_LAND_LOAD" || RET=1
validate_misc "Test" "TEST" || RET=1
validate_misc "World_Conquest" "MULTIPLAYER,LOAD_WC2,LOAD_WC2_EVEN_THOUGH_IT_NEEDS_A_NEW_MAINTAINER" || RET=1
validate_campaign "Dead_Water" "CAMPAIGN_DEAD_WATER" "EASY" "NORMAL" "HARD" "NIGHTMARE" || RET=1
validate_campaign "Delfadors_Memoirs" "CAMPAIGN_DELFADORS_MEMOIRS" "EASY" "NORMAL" "HARD" || RET=1
validate_campaign "Descent_Into_Darkness" "CAMPAIGN_DESCENT" "EASY" "NORMAL" "HARD" || RET=1

View file

@ -202,6 +202,7 @@
9 test_store_unit_defense_deprecated
0 special_note_from_movetype
0 special_note_individual_unit
0 has_achievement
# Terrain mask tests
0 test_terrain_mask_simple_nop
0 test_terrain_mask_simple_set