GUI2: initial basic implementation of a GUI2 Attack Predictions dialog (WIP)
This commit is contained in:
parent
76cfdd589d
commit
fc91f5df10
7 changed files with 626 additions and 2 deletions
338
data/gui/window/attack_predictions.cfg
Normal file
338
data/gui/window/attack_predictions.cfg
Normal file
|
@ -0,0 +1,338 @@
|
|||
#textdomain wesnoth-lib
|
||||
###
|
||||
### Definition of the window to display various possible attack results
|
||||
###
|
||||
|
||||
#define _GUI_SPACER_ROW
|
||||
[row]
|
||||
grow_factor = 0
|
||||
|
||||
[column]
|
||||
#border = "all"
|
||||
#border_size = 5
|
||||
|
||||
[spacer]
|
||||
height = 15
|
||||
[/spacer]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
#enddef
|
||||
|
||||
#define _GUI_DATA_PANEL _HEADER _ID_PREFIX
|
||||
[grid]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
label = {_HEADER}
|
||||
definition = "default_bold"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_attack"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_resis"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
{_GUI_SPACER_ROW}
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_damage"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_no_damage_chance"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[/grid]
|
||||
#enddef
|
||||
|
||||
[window]
|
||||
id = "attack_predictions"
|
||||
description = "Statistics dialog."
|
||||
|
||||
[resolution]
|
||||
definition = "default"
|
||||
|
||||
automatic_placement = "true"
|
||||
vertical_placement = "center"
|
||||
horizontal_placement = "center"
|
||||
|
||||
maximum_height = 400
|
||||
|
||||
[linked_group]
|
||||
id = "header"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "attack"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "resis"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "damage"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "chance_unscathed"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "bonuses"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[tooltip]
|
||||
id = "tooltip"
|
||||
[/tooltip]
|
||||
|
||||
[helptip]
|
||||
id = "tooltip"
|
||||
[/helptip]
|
||||
|
||||
[grid]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
grow_factor = 1
|
||||
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = "title"
|
||||
definition = "title"
|
||||
|
||||
label = _ "Damage Calculations"
|
||||
[/label]
|
||||
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
grow_factor = 0
|
||||
|
||||
[column]
|
||||
horizontal_grow = "true"
|
||||
vertical_grow = "true"
|
||||
|
||||
[grid]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
grow_factor = 0
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[grid]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
|
||||
[spacer]
|
||||
linked_group = "header"
|
||||
[/spacer]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
label = _ "Base damage"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
#label = _ "Opponent resistance against"
|
||||
label = _ "Modifiers"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
{_GUI_SPACER_ROW}
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
label = _ "Total damage"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
label = _ "Chance of being unscathed"
|
||||
definition = "default"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[/grid]
|
||||
|
||||
[/column]
|
||||
|
||||
[column]
|
||||
grow_factor = 1
|
||||
horizontal_grow = "true"
|
||||
|
||||
{_GUI_DATA_PANEL ( _ "Attacker") ("attacker")}
|
||||
[/column]
|
||||
|
||||
[column]
|
||||
grow_factor = 1
|
||||
horizontal_grow = "true"
|
||||
|
||||
{_GUI_DATA_PANEL ( _ "Defender") ("defender")}
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[/grid]
|
||||
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
grow_factor = 0
|
||||
|
||||
[column]
|
||||
border = "all"
|
||||
border_size = 5
|
||||
horizontal_alignment = "right"
|
||||
|
||||
[button]
|
||||
id = "cancel"
|
||||
label = _ "Close"
|
||||
[/button]
|
||||
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[/grid]
|
||||
|
||||
[/resolution]
|
||||
|
||||
[/window]
|
||||
|
||||
#undef _GUI_DATA_PANEL
|
||||
#undef _GUI_SPACER_ROW
|
|
@ -525,6 +525,8 @@
|
|||
<Unit filename="../../src/gui/dialogs/addon/uninstall_list.hpp" />
|
||||
<Unit filename="../../src/gui/dialogs/advanced_graphics_options.cpp" />
|
||||
<Unit filename="../../src/gui/dialogs/advanced_graphics_options.hpp" />
|
||||
<Unit filename="../../src/gui/dialogs/attack_predictions.cpp" />
|
||||
<Unit filename="../../src/gui/dialogs/attack_predictions.hpp" />
|
||||
<Unit filename="../../src/gui/dialogs/campaign_difficulty.cpp" />
|
||||
<Unit filename="../../src/gui/dialogs/campaign_difficulty.hpp" />
|
||||
<Unit filename="../../src/gui/dialogs/campaign_selection.cpp" />
|
||||
|
|
|
@ -167,6 +167,7 @@ gui/dialogs/addon/filter_options.cpp
|
|||
gui/dialogs/addon/manager.cpp
|
||||
gui/dialogs/addon/uninstall_list.cpp
|
||||
gui/dialogs/advanced_graphics_options.cpp
|
||||
gui/dialogs/attack_predictions.cpp
|
||||
gui/dialogs/campaign_difficulty.cpp
|
||||
gui/dialogs/campaign_selection.cpp
|
||||
gui/dialogs/campaign_settings.cpp
|
||||
|
|
205
src/gui/dialogs/attack_predictions.cpp
Normal file
205
src/gui/dialogs/attack_predictions.cpp
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
Copyright (C) 2010 - 2016 by Mark de Wever <koraq@xs4all.nl>
|
||||
Part of the Battle for Wesnoth Project http://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/attack_predictions.hpp"
|
||||
|
||||
#include "attack_prediction.hpp"
|
||||
#include "color.hpp"
|
||||
#include "font/standard_colors.hpp"
|
||||
#include "font/text_formatting.hpp"
|
||||
#include "formatter.hpp"
|
||||
#include "game_config.hpp"
|
||||
#include "gui/auxiliary/find_widget.hpp"
|
||||
#include "gui/widgets/label.hpp"
|
||||
#include "gui/widgets/image.hpp"
|
||||
#include "gui/widgets/settings.hpp"
|
||||
#include "gui/widgets/window.hpp"
|
||||
#include "gettext.hpp"
|
||||
#include "language.hpp"
|
||||
#include "units/abilities.hpp"
|
||||
#include "units/unit.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
|
||||
namespace gui2
|
||||
{
|
||||
namespace dialogs
|
||||
{
|
||||
|
||||
REGISTER_DIALOG(attack_predictions)
|
||||
|
||||
attack_predictions::attack_predictions(battle_context& bc, const unit& attacker, const unit& defender)
|
||||
: attacker_data_(attacker, bc.get_attacker_combatant(), bc.get_attacker_stats())
|
||||
, defender_data_(defender, bc.get_defender_combatant(), bc.get_defender_stats())
|
||||
{
|
||||
}
|
||||
|
||||
void attack_predictions::pre_show(window& window)
|
||||
{
|
||||
set_data(window, attacker_data_, defender_data_);
|
||||
set_data(window, defender_data_, attacker_data_);
|
||||
}
|
||||
|
||||
static std::string get_probability_string(const double prob)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
|
||||
if(prob > 0.9995) {
|
||||
ss << "100%";
|
||||
} else {
|
||||
ss << std::fixed << std::setprecision(1) << 100.0 * prob << '%';
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
void attack_predictions::set_data(window& window, const combatant_data& attacker, const combatant_data& defender)
|
||||
{
|
||||
const std::string widget_id_prefix = attacker.stats.is_attacker ? "attacker_" : "defender_";
|
||||
|
||||
const auto set_label_helper = [&](const std::string& id, const std::string& value) {
|
||||
find_widget<label>(&window, widget_id_prefix + id, false).set_label(value);
|
||||
};
|
||||
|
||||
std::stringstream ss;
|
||||
|
||||
// With a weapon.
|
||||
if(attacker.stats.weapon) {
|
||||
// Set specials context (for safety, it should not have changed normally).
|
||||
const attack_type* weapon = attacker.stats.weapon;
|
||||
weapon->set_specials_context(attacker.unit.get_location(), defender.unit.get_location(), attacker.stats.is_attacker, defender.stats.weapon);
|
||||
|
||||
// Get damage modifiers.
|
||||
unit_ability_list dmg_specials = weapon->get_specials("damage");
|
||||
unit_abilities::effect dmg_effect(dmg_specials, weapon->damage(), attacker.stats.backstab_pos);
|
||||
|
||||
// Get the SET damage modifier, if any.
|
||||
const unit_abilities::individual_effect* set_dmg_effect = &*(
|
||||
std::find_if(dmg_effect.begin(), dmg_effect.end(), [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; })
|
||||
);
|
||||
|
||||
// Either user the SET modifier or the base weapon damage.
|
||||
if(!set_dmg_effect) {
|
||||
// TODO: formatting
|
||||
ss << weapon->name() << ": " << weapon->damage();
|
||||
} else {
|
||||
assert(set_dmg_effect->ability);
|
||||
ss << (*set_dmg_effect->ability)["name"] << ": " << set_dmg_effect->value;
|
||||
}
|
||||
|
||||
// Process the ADD damage modifiers.
|
||||
for(const auto& e : dmg_effect) {
|
||||
if(e.type == unit_abilities::ADD) {
|
||||
ss << "\n";
|
||||
ss << (*e.ability)["name"] << ": ";
|
||||
|
||||
if(e.value >= 0) {
|
||||
ss << '+';
|
||||
}
|
||||
|
||||
ss << e.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the MUL damage modifiers.
|
||||
for(const auto& e : dmg_effect) {
|
||||
if(e.type == unit_abilities::MUL) {
|
||||
ss << "\n";
|
||||
ss << (*e.ability)["name"] << ": " << font::unicode_multiplication_sign << (e.value / 100);
|
||||
|
||||
if(e.value % 100) {
|
||||
ss << "." << ((e.value % 100) / 10);
|
||||
if(e.value % 10) {
|
||||
ss << (e.value % 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_label_helper("attack", ss.str());
|
||||
|
||||
#if 0
|
||||
// Time of day modifier.
|
||||
int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(), attacker.unit.get_location(), u.alignment(), u.is_fearless());
|
||||
if(tod_modifier != 0) {
|
||||
left_strings.push_back(_("Time of day"));
|
||||
str.str("");
|
||||
ss << utils::signed_percent(tod_modifier);
|
||||
right_strings.push_back(str.str());
|
||||
}
|
||||
|
||||
// Leadership bonus.
|
||||
int leadership_bonus = 0;
|
||||
under_leadership(resources::gameboard->units(), attacker.unit.get_location(), &leadership_bonus);
|
||||
if(leadership_bonus != 0) {
|
||||
left_strings.push_back(_("Leadership"));
|
||||
str.str("");
|
||||
ss << utils::signed_percent(leadership_bonus);
|
||||
right_strings.push_back(str.str());
|
||||
}
|
||||
#endif
|
||||
|
||||
ss.str("");
|
||||
|
||||
// Resistance modifier.
|
||||
const int resistance_modifier = defender.unit.damage_from(*weapon, !attacker.stats.is_attacker, defender.unit.get_location());
|
||||
if(resistance_modifier != 100) {
|
||||
ss << string_table["type_" + weapon->type()] << ": ";
|
||||
ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
|
||||
|
||||
set_label_helper("resis", ss.str());
|
||||
}
|
||||
|
||||
// Slowed penalty.
|
||||
#if 0
|
||||
if(attacker.stats.is_slowed) {
|
||||
left_strings.push_back(_("Slowed"));
|
||||
right_strings.push_back("/ 2");
|
||||
}
|
||||
#endif
|
||||
|
||||
ss.str("");
|
||||
|
||||
// Total damage.
|
||||
const int base_damage = weapon->damage();
|
||||
|
||||
color_t dmg_color = font::weapon_color;
|
||||
if(attacker.stats.damage > base_damage) {
|
||||
dmg_color = font::good_dmg_color;
|
||||
} else if(attacker.stats.damage < base_damage) {
|
||||
dmg_color = font::bad_dmg_color;
|
||||
}
|
||||
|
||||
const color_t cth_color = game_config::red_to_green(attacker.stats.chance_to_hit);
|
||||
|
||||
ss << font::span_color(dmg_color) << attacker.stats.damage << "</span>"
|
||||
<< font::weapon_numbers_sep << attacker.stats.num_blows << " ("
|
||||
<< font::span_color(cth_color) << attacker.stats.chance_to_hit << "%</span>)";
|
||||
|
||||
set_label_helper("damage", ss.str());
|
||||
} else {
|
||||
// Without a weapon.
|
||||
set_label_helper("attack", _("No usable weapon"));
|
||||
}
|
||||
|
||||
const color_t ndc_color = game_config::red_to_green(attacker.combatant.untouched * 10);
|
||||
|
||||
// Unscathed probability.
|
||||
set_label_helper("no_damage_chance", (formatter() << font::span_color(ndc_color) << get_probability_string(attacker.combatant.untouched) << "</span>").str());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
72
src/gui/dialogs/attack_predictions.hpp
Normal file
72
src/gui/dialogs/attack_predictions.hpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright (C) 2010 - 2016 by Mark de Wever <koraq@xs4all.nl>
|
||||
Part of the Battle for Wesnoth Project http://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.
|
||||
*/
|
||||
|
||||
#ifndef GUI_DIALOGS_ATTACK_PREDICTIONS_HPP_INCLUDED
|
||||
#define GUI_DIALOGS_ATTACK_PREDICTIONS_HPP_INCLUDED
|
||||
|
||||
#include "actions/attack.hpp"
|
||||
#include "gui/dialogs/modal_dialog.hpp"
|
||||
#include "units/map.hpp"
|
||||
|
||||
class battle_context;
|
||||
class battle_context_unit_stats;
|
||||
class CVideo;
|
||||
class map_location;
|
||||
struct combatant;
|
||||
|
||||
namespace gui2
|
||||
{
|
||||
namespace dialogs
|
||||
{
|
||||
|
||||
class attack_predictions : public modal_dialog
|
||||
{
|
||||
public:
|
||||
attack_predictions(battle_context& bc, const unit& attacker, const unit& defender);
|
||||
|
||||
static void display(battle_context& bc, const unit& attacker, const unit& defender, CVideo& video)
|
||||
{
|
||||
attack_predictions(bc, attacker, defender).show(video);
|
||||
}
|
||||
|
||||
private:
|
||||
/** Inherited from modal_dialog, implemented by REGISTER_DIALOG. */
|
||||
virtual const std::string& window_id() const;
|
||||
|
||||
/** Inherited from modal_dialog. */
|
||||
void pre_show(window& window);
|
||||
|
||||
struct combatant_data
|
||||
{
|
||||
combatant_data(const unit& unit, const combatant& combatant, const battle_context_unit_stats& stats)
|
||||
: stats(stats)
|
||||
, combatant(combatant)
|
||||
, unit(unit)
|
||||
{}
|
||||
|
||||
const battle_context_unit_stats& stats;
|
||||
const combatant& combatant;
|
||||
const unit& unit;
|
||||
};
|
||||
|
||||
void set_data(window& window, const combatant_data& attacker, const combatant_data& defender);
|
||||
|
||||
combatant_data attacker_data_;
|
||||
combatant_data defender_data_;
|
||||
};
|
||||
|
||||
} // namespace dialogs
|
||||
} // namespace gui2
|
||||
|
||||
#endif
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
#include "font/text_formatting.hpp"
|
||||
#include "gui/auxiliary/find_widget.hpp"
|
||||
#include "gui/dialogs/attack_predictions.hpp"
|
||||
#include "gui/widgets/button.hpp"
|
||||
#include "gui/widgets/label.hpp"
|
||||
#include "gui/widgets/image.hpp"
|
||||
|
@ -90,9 +91,11 @@ unit_attack::unit_attack(const unit_map::iterator& attacker_itor,
|
|||
|
||||
void unit_attack::damage_calc_callback(window& window)
|
||||
{
|
||||
const size_t index
|
||||
= find_widget<listbox>(&window, "weapon_list", false).get_selected_row();
|
||||
const size_t index = find_widget<listbox>(&window, "weapon_list", false).get_selected_row();
|
||||
|
||||
attack_predictions::display(weapons_[index], *attacker_itor_, *defender_itor_, window.video());
|
||||
|
||||
// TODO: remove when the GUI2 dialog is complete
|
||||
battle_prediction_pane battle_pane(weapons_[index], (*attacker_itor_).get_location(), (*defender_itor_).get_location());
|
||||
std::vector<gui::preview_pane*> preview_panes = {&battle_pane};
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
#include "gui/dialogs/addon/connect.hpp"
|
||||
#include "gui/dialogs/addon/manager.hpp"
|
||||
#include "gui/dialogs/advanced_graphics_options.hpp"
|
||||
#include "gui/dialogs/attack_predictions.hpp"
|
||||
#include "gui/dialogs/campaign_difficulty.hpp"
|
||||
#include "gui/dialogs/campaign_selection.hpp"
|
||||
#include "gui/dialogs/campaign_settings.hpp"
|
||||
|
@ -388,6 +389,7 @@ BOOST_AUTO_TEST_CASE(test_gui2)
|
|||
test<addon_connect>();
|
||||
//test<addon_manager>();
|
||||
test<advanced_graphics_options>();
|
||||
//test<attack_predictions>();
|
||||
test<campaign_difficulty>();
|
||||
test<campaign_selection>();
|
||||
test<campaign_settings>();
|
||||
|
@ -502,6 +504,7 @@ BOOST_AUTO_TEST_CASE(test_gui2)
|
|||
"mp_staging",
|
||||
"mp_join_game",
|
||||
"terrain_layers",
|
||||
"attack_predictions",
|
||||
};
|
||||
std::sort(list.begin(), list.end());
|
||||
std::sort(omitted.begin(), omitted.end());
|
||||
|
|
Loading…
Add table
Reference in a new issue