develop
commit
72a4698968
@ -0,0 +1,534 @@
|
|||||||
|
#include "Core.h"
|
||||||
|
#include <Console.h>
|
||||||
|
#include <Export.h>
|
||||||
|
#include <PluginManager.h>
|
||||||
|
#include <MiscUtils.h>
|
||||||
|
|
||||||
|
#include <modules/Materials.h>
|
||||||
|
|
||||||
|
#include <DataDefs.h>
|
||||||
|
#include <df/world.h>
|
||||||
|
#include <df/ui.h>
|
||||||
|
#include <df/ui_build_selector.h>
|
||||||
|
#include <df/ui_build_item_req.h>
|
||||||
|
#include <df/build_req_choice_genst.h>
|
||||||
|
#include <df/build_req_choice_specst.h>
|
||||||
|
#include <df/building_workshopst.h>
|
||||||
|
#include <df/building_furnacest.h>
|
||||||
|
#include <df/job.h>
|
||||||
|
#include <df/job_item.h>
|
||||||
|
#include <df/job_list_link.h>
|
||||||
|
#include <df/item.h>
|
||||||
|
#include <df/tool_uses.h>
|
||||||
|
#include <df/general_ref.h>
|
||||||
|
#include <df/general_ref_unit_workerst.h>
|
||||||
|
|
||||||
|
using std::vector;
|
||||||
|
using std::string;
|
||||||
|
using std::endl;
|
||||||
|
using namespace DFHack;
|
||||||
|
using namespace df::enums;
|
||||||
|
|
||||||
|
using df::global::world;
|
||||||
|
using df::global::ui;
|
||||||
|
using df::global::ui_build_selector;
|
||||||
|
using df::global::ui_workshop_job_cursor;
|
||||||
|
using df::global::job_next_id;
|
||||||
|
|
||||||
|
/* Plugin registration */
|
||||||
|
|
||||||
|
static bool workshop_job_hotkey(Core *c, df::viewscreen *top);
|
||||||
|
static bool build_selector_hotkey(Core *c, df::viewscreen *top);
|
||||||
|
static bool job_material_hotkey(Core *c, df::viewscreen *top);
|
||||||
|
|
||||||
|
static command_result job_material(Core *c, vector <string> & parameters);
|
||||||
|
static command_result job_duplicate(Core *c, vector <string> & parameters);
|
||||||
|
static command_result job_cmd(Core *c, vector <string> & parameters);
|
||||||
|
|
||||||
|
DFhackCExport const char * plugin_name ( void )
|
||||||
|
{
|
||||||
|
return "jobutils";
|
||||||
|
}
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_init (Core *c, std::vector <PluginCommand> &commands)
|
||||||
|
{
|
||||||
|
commands.clear();
|
||||||
|
if (!world || !ui)
|
||||||
|
return CR_FAILURE;
|
||||||
|
|
||||||
|
commands.push_back(
|
||||||
|
PluginCommand(
|
||||||
|
"job", "General job query and manipulation.",
|
||||||
|
job_cmd, false,
|
||||||
|
" job query\n"
|
||||||
|
" Print details of the current job.\n"
|
||||||
|
" job list\n"
|
||||||
|
" Print details of all jobs in the workshop.\n"
|
||||||
|
" job item-material <item-idx> <material> [submaterial]\n"
|
||||||
|
" Replace the exact material id in the job item.\n"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ui_workshop_job_cursor || ui_build_selector) {
|
||||||
|
commands.push_back(
|
||||||
|
PluginCommand(
|
||||||
|
"job-material", "Alter the material of the selected job.",
|
||||||
|
job_material, job_material_hotkey,
|
||||||
|
" job-material <inorganic-token>\n"
|
||||||
|
"Intended to be used as a keybinding:\n"
|
||||||
|
" - In 'q' mode, when a job is highlighted within a workshop\n"
|
||||||
|
" or furnace, changes the material of the job.\n"
|
||||||
|
" - In 'b' mode, during selection of building components\n"
|
||||||
|
" positions the cursor over the first available choice\n"
|
||||||
|
" with the matching material.\n"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ui_workshop_job_cursor && job_next_id) {
|
||||||
|
commands.push_back(
|
||||||
|
PluginCommand(
|
||||||
|
"job-duplicate", "Duplicate the selected job in a workshop.",
|
||||||
|
job_duplicate, workshop_job_hotkey,
|
||||||
|
" - In 'q' mode, when a job is highlighted within a workshop\n"
|
||||||
|
" or furnace building, instantly duplicates the job.\n"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_shutdown ( Core * c )
|
||||||
|
{
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* UI state guards */
|
||||||
|
|
||||||
|
static bool workshop_job_hotkey(Core *c, df::viewscreen *top)
|
||||||
|
{
|
||||||
|
using namespace ui_sidebar_mode;
|
||||||
|
|
||||||
|
if (!dwarfmode_hotkey(c,top))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (ui->main.mode) {
|
||||||
|
case QueryBuilding:
|
||||||
|
{
|
||||||
|
if (!ui_workshop_job_cursor) // allow missing
|
||||||
|
return false;
|
||||||
|
|
||||||
|
df::building *selected = world->selected_building;
|
||||||
|
if (!virtual_cast<df::building_workshopst>(selected) &&
|
||||||
|
!virtual_cast<df::building_furnacest>(selected))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// No jobs?
|
||||||
|
if (selected->jobs.empty() ||
|
||||||
|
selected->jobs[0]->job_type == job_type::DestroyBuilding)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Add job gui activated?
|
||||||
|
if (df::global::ui_workshop_in_add && // allow missing
|
||||||
|
*df::global::ui_workshop_in_add)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool build_selector_hotkey(Core *c, df::viewscreen *top)
|
||||||
|
{
|
||||||
|
using namespace ui_sidebar_mode;
|
||||||
|
|
||||||
|
if (!dwarfmode_hotkey(c,top))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (ui->main.mode) {
|
||||||
|
case Build:
|
||||||
|
{
|
||||||
|
if (!ui_build_selector) // allow missing
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Not selecting, or no choices?
|
||||||
|
if (ui_build_selector->building_type < 0 ||
|
||||||
|
ui_build_selector->stage != 2 ||
|
||||||
|
ui_build_selector->choices.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool job_material_hotkey(Core *c, df::viewscreen *top)
|
||||||
|
{
|
||||||
|
return workshop_job_hotkey(c, top) ||
|
||||||
|
build_selector_hotkey(c, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* job-material implementation */
|
||||||
|
|
||||||
|
static df::job *getWorkshopJob(Core *c)
|
||||||
|
{
|
||||||
|
df::building *selected = world->selected_building;
|
||||||
|
int idx = *ui_workshop_job_cursor;
|
||||||
|
|
||||||
|
if (idx < 0 || idx >= selected->jobs.size())
|
||||||
|
{
|
||||||
|
c->con.printerr("Invalid job cursor index: %d\n", idx);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected->jobs[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result job_material_in_job(Core *c, MaterialInfo &new_mat)
|
||||||
|
{
|
||||||
|
df::job *job = getWorkshopJob(c);
|
||||||
|
if (!job)
|
||||||
|
return CR_FAILURE;
|
||||||
|
|
||||||
|
MaterialInfo cur_mat(job);
|
||||||
|
|
||||||
|
if (!cur_mat.isValid() || cur_mat.type != 0)
|
||||||
|
{
|
||||||
|
c->con.printerr("Current job material isn't inorganic: %s\n",
|
||||||
|
cur_mat.toString().c_str());
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
df::craft_material_class old_class = cur_mat.getCraftClass();
|
||||||
|
if (old_class == craft_material_class::None)
|
||||||
|
{
|
||||||
|
c->con.printerr("Unexpected current material type: %s\n",
|
||||||
|
cur_mat.toString().c_str());
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
if (new_mat.getCraftClass() != old_class)
|
||||||
|
{
|
||||||
|
c->con.printerr("New material %s does not satisfy requirement: %s\n",
|
||||||
|
new_mat.toString().c_str(), ENUM_KEY_STR(craft_material_class, old_class));
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < job->job_items.size(); i++)
|
||||||
|
{
|
||||||
|
df::job_item *item = job->job_items[i];
|
||||||
|
MaterialInfo item_mat(item);
|
||||||
|
|
||||||
|
if (item_mat != cur_mat)
|
||||||
|
{
|
||||||
|
c->con.printerr("Job item %d has different material: %s\n",
|
||||||
|
i, item_mat.toString().c_str());
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the substitution
|
||||||
|
job->mat_type = new_mat.type;
|
||||||
|
job->mat_index = new_mat.index;
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < job->job_items.size(); i++)
|
||||||
|
{
|
||||||
|
df::job_item *item = job->job_items[i];
|
||||||
|
item->mat_type = new_mat.type;
|
||||||
|
item->mat_index = new_mat.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
c->con << "Applied material '" << new_mat.toString()
|
||||||
|
<< "' to job " << ENUM_KEY_STR(job_type,job->job_type) << endl;
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool build_choice_matches(df::ui_build_item_req *req, df::build_req_choicest *choice,
|
||||||
|
MaterialInfo &new_mat)
|
||||||
|
{
|
||||||
|
if (VIRTUAL_CAST_VAR(gen, df::build_req_choice_genst, choice))
|
||||||
|
{
|
||||||
|
if (gen->mat_type == new_mat.type &&
|
||||||
|
gen->mat_index == new_mat.index &&
|
||||||
|
gen->used_count < gen->candidates.size())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (VIRTUAL_CAST_VAR(spec, df::build_req_choice_specst, choice))
|
||||||
|
{
|
||||||
|
if (spec->candidate &&
|
||||||
|
spec->candidate->getActualMaterial() == new_mat.type &&
|
||||||
|
spec->candidate->getActualMaterialIndex() == new_mat.index &&
|
||||||
|
!req->candidate_selected[spec->candidate_id])
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result job_material_in_build(Core *c, MaterialInfo &new_mat)
|
||||||
|
{
|
||||||
|
df::ui_build_item_req *req = ui_build_selector->requirements[ui_build_selector->req_index];
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < ui_build_selector->choices.size(); i++)
|
||||||
|
{
|
||||||
|
if (build_choice_matches(req, ui_build_selector->choices[i], new_mat))
|
||||||
|
{
|
||||||
|
ui_build_selector->sel_index = i;
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c->con.printerr("Could not find material in list: %s\n", new_mat.toString().c_str());
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result job_material(Core * c, vector <string> & parameters)
|
||||||
|
{
|
||||||
|
// HOTKEY COMMAND: CORE ALREADY SUSPENDED
|
||||||
|
|
||||||
|
MaterialInfo new_mat;
|
||||||
|
if (parameters.size() == 1)
|
||||||
|
{
|
||||||
|
if (!new_mat.findInorganic(parameters[0])) {
|
||||||
|
c->con.printerr("Could not find inorganic material: %s\n", parameters[0].c_str());
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
if (ui->main.mode == ui_sidebar_mode::QueryBuilding)
|
||||||
|
return job_material_in_job(c, new_mat);
|
||||||
|
if (ui->main.mode == ui_sidebar_mode::Build)
|
||||||
|
return job_material_in_build(c, new_mat);
|
||||||
|
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* job-duplicate implementation */
|
||||||
|
|
||||||
|
static df::job *clone_job(df::job *job)
|
||||||
|
{
|
||||||
|
df::job *pnew = new df::job(*job);
|
||||||
|
|
||||||
|
pnew->id = (*job_next_id)++;
|
||||||
|
|
||||||
|
// Clean out transient fields
|
||||||
|
pnew->flags.whole = 0;
|
||||||
|
pnew->flags.bits.repeat = job->flags.bits.repeat;
|
||||||
|
|
||||||
|
pnew->completion_timer = -1;
|
||||||
|
pnew->items.clear();
|
||||||
|
pnew->misc_links.clear();
|
||||||
|
|
||||||
|
// Link the job into the global list
|
||||||
|
pnew->list_link = new df::job_list_link();
|
||||||
|
pnew->list_link->item = pnew;
|
||||||
|
|
||||||
|
linked_list_append(&world->job_list, pnew->list_link);
|
||||||
|
|
||||||
|
// Clone refs
|
||||||
|
for (int i = pnew->references.size()-1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
df::general_ref *ref = pnew->references[i];
|
||||||
|
|
||||||
|
if (virtual_cast<df::general_ref_unit_workerst>(ref))
|
||||||
|
pnew->references.erase(pnew->references.begin()+i);
|
||||||
|
else
|
||||||
|
pnew->references[i] = ref->clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone items
|
||||||
|
for (int i = pnew->job_items.size()-1; i >= 0; i--)
|
||||||
|
pnew->job_items[i] = new df::job_item(*pnew->job_items[i]);
|
||||||
|
|
||||||
|
return pnew;
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result job_duplicate(Core * c, vector <string> & parameters)
|
||||||
|
{
|
||||||
|
if (!parameters.empty())
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
df::job *job = getWorkshopJob(c);
|
||||||
|
if (!job)
|
||||||
|
return CR_FAILURE;
|
||||||
|
|
||||||
|
if (!job->misc_links.empty() || job->job_items.empty())
|
||||||
|
{
|
||||||
|
c->con.printerr("Cannot duplicate job %s\n", ENUM_KEY_STR(job_type,job->job_type));
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
df::building *building = world->selected_building;
|
||||||
|
if (building->jobs.size() >= 10)
|
||||||
|
{
|
||||||
|
c->con.printerr("Job list is already full.\n");
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually clone
|
||||||
|
df::job *pnew = clone_job(job);
|
||||||
|
|
||||||
|
int pos = ++*ui_workshop_job_cursor;
|
||||||
|
building->jobs.insert(building->jobs.begin()+pos, pnew);
|
||||||
|
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main job command implementation */
|
||||||
|
|
||||||
|
static void print_job_item_details(Core *c, df::job *job, unsigned idx, df::job_item *item)
|
||||||
|
{
|
||||||
|
c->con << " Input Item " << (idx+1) << ": " << ENUM_KEY_STR(item_type,item->item_type);
|
||||||
|
if (item->item_subtype != -1)
|
||||||
|
c->con << " [" << item->item_subtype << "]";
|
||||||
|
if (item->quantity != 1)
|
||||||
|
c->con << "; quantity=" << item->quantity;
|
||||||
|
if (item->min_dimension >= 0)
|
||||||
|
c->con << "; min_dimension=" << item->min_dimension;
|
||||||
|
c->con << endl;
|
||||||
|
|
||||||
|
MaterialInfo mat(item);
|
||||||
|
if (mat.isValid() || item->metal_ore >= 0) {
|
||||||
|
c->con << " material: " << mat.toString();
|
||||||
|
if (item->metal_ore >= 0)
|
||||||
|
c->con << "; ore of " << MaterialInfo(0,item->metal_ore).toString();
|
||||||
|
c->con << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item->flags1.whole)
|
||||||
|
c->con << " flags1: " << bitfieldToString(item->flags1) << endl;
|
||||||
|
if (item->flags2.whole)
|
||||||
|
c->con << " flags2: " << bitfieldToString(item->flags2) << endl;
|
||||||
|
if (item->flags3.whole)
|
||||||
|
c->con << " flags3: " << bitfieldToString(item->flags3) << endl;
|
||||||
|
|
||||||
|
if (!item->reaction_class.empty())
|
||||||
|
c->con << " reaction class: " << item->reaction_class << endl;
|
||||||
|
if (!item->has_material_reaction_product.empty())
|
||||||
|
c->con << " reaction product: " << item->has_material_reaction_product << endl;
|
||||||
|
if (item->has_tool_use >= 0)
|
||||||
|
c->con << " tool use: " << ENUM_KEY_STR(tool_uses, item->has_tool_use) << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_job_details(Core *c, df::job *job)
|
||||||
|
{
|
||||||
|
c->con << "Job " << job->id << ": " << ENUM_KEY_STR(job_type,job->job_type);
|
||||||
|
if (job->flags.whole)
|
||||||
|
c->con << " (" << bitfieldToString(job->flags) << ")";
|
||||||
|
c->con << endl;
|
||||||
|
|
||||||
|
MaterialInfo mat(job);
|
||||||
|
if (mat.isValid() || job->material_category.whole)
|
||||||
|
{
|
||||||
|
c->con << " material: " << mat.toString();
|
||||||
|
if (job->material_category.whole)
|
||||||
|
c->con << " (" << bitfieldToString(job->material_category) << ")";
|
||||||
|
c->con << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job->item_subtype >= 0 || job->item_category.whole)
|
||||||
|
c->con << " item: " << job->item_subtype
|
||||||
|
<< " (" << bitfieldToString(job->item_category) << ")" << endl;
|
||||||
|
|
||||||
|
if (job->hist_figure_id >= 0)
|
||||||
|
c->con << " figure: " << job->hist_figure_id << endl;
|
||||||
|
|
||||||
|
if (!job->reaction_name.empty())
|
||||||
|
c->con << " reaction: " << job->reaction_name << endl;
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < job->job_items.size(); i++)
|
||||||
|
print_job_item_details(c, job, i, job->job_items[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static df::job *getWorkshopJobSafe(Core *c)
|
||||||
|
{
|
||||||
|
if (!workshop_job_hotkey(c, c->getTopViewscreen())) {
|
||||||
|
c->con.printerr("No job is highlighted.\n");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getWorkshopJob(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result job_cmd(Core * c, vector <string> & parameters)
|
||||||
|
{
|
||||||
|
CoreSuspender suspend(c);
|
||||||
|
|
||||||
|
if (parameters.empty())
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
std::string cmd = parameters[0];
|
||||||
|
if (cmd == "query" || cmd == "list")
|
||||||
|
{
|
||||||
|
df::job *job = getWorkshopJobSafe(c);
|
||||||
|
if (!job)
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
if (cmd == "query") {
|
||||||
|
print_job_details(c, job);
|
||||||
|
} else {
|
||||||
|
df::building *selected = world->selected_building;
|
||||||
|
for (unsigned i = 0; i < selected->jobs.size(); i++)
|
||||||
|
print_job_details(c, selected->jobs[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (cmd == "item-material")
|
||||||
|
{
|
||||||
|
if (parameters.size() < 1+1+1)
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
df::job *job = getWorkshopJobSafe(c);
|
||||||
|
if (!job)
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
int v = atoi(parameters[1].c_str());
|
||||||
|
if (v < 1 || v > job->job_items.size()) {
|
||||||
|
c->con.printerr("Invalid item index.\n");
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
df::job_item *item = job->job_items[v-1];
|
||||||
|
|
||||||
|
std::string subtoken = (parameters.size()>3 ? parameters[3] : "");
|
||||||
|
MaterialInfo info;
|
||||||
|
if (!info.find(parameters[2], subtoken)) {
|
||||||
|
c->con.printerr("Could not find the specified material.\n");
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.matches(*item)) {
|
||||||
|
c->con.printerr("Material does not match the requirements.\n");
|
||||||
|
print_job_details(c, job);
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job->mat_type != -1 &&
|
||||||
|
job->mat_type == item->mat_type &&
|
||||||
|
job->mat_index == item->mat_index)
|
||||||
|
{
|
||||||
|
job->mat_type = info.type;
|
||||||
|
job->mat_index = info.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
item->mat_type = info.type;
|
||||||
|
item->mat_index = info.index;
|
||||||
|
|
||||||
|
c->con << "Job item " << v << " updated." << endl;
|
||||||
|
print_job_details(c, job);
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
return CR_OK;
|
||||||
|
}
|
Loading…
Reference in New Issue