mirror of
https://github.com/OpenMW/openmw.git
synced 2025-10-18 19:16:37 +00:00
Merge branch 'master' of https://gitlab.com/OpenMW/openmw
This commit is contained in:
commit
e4cc3adc98
9 changed files with 186 additions and 15 deletions
|
@ -75,6 +75,7 @@
|
||||||
Feature #8580: Sort characters in the save loading menu
|
Feature #8580: Sort characters in the save loading menu
|
||||||
Feature #8597: Lua: Add more built-in event handlers
|
Feature #8597: Lua: Add more built-in event handlers
|
||||||
Feature #8629: Expose path grid data to Lua
|
Feature #8629: Expose path grid data to Lua
|
||||||
|
Feature #8654: Allow lua world.createRecord to create NPC records
|
||||||
|
|
||||||
0.49.0
|
0.49.0
|
||||||
------
|
------
|
||||||
|
|
|
@ -645,6 +645,9 @@ namespace MWLua
|
||||||
}
|
}
|
||||||
inventoryT["isResolved"] = [](const InventoryT& inventory) -> bool {
|
inventoryT["isResolved"] = [](const InventoryT& inventory) -> bool {
|
||||||
const MWWorld::Ptr& ptr = inventory.mObj.ptr();
|
const MWWorld::Ptr& ptr = inventory.mObj.ptr();
|
||||||
|
// Avoid initializing custom data
|
||||||
|
if (!ptr.getRefData().getCustomData())
|
||||||
|
return false;
|
||||||
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
|
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
|
||||||
return store.isResolved();
|
return store.isResolved();
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
#include "../context.hpp"
|
#include "../context.hpp"
|
||||||
|
|
||||||
|
#include "servicesoffered.hpp"
|
||||||
namespace MWLua
|
namespace MWLua
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -25,15 +26,6 @@ namespace MWLua
|
||||||
record["servicesOffered"] = sol::readonly_property([context](const T& rec) -> sol::table {
|
record["servicesOffered"] = sol::readonly_property([context](const T& rec) -> sol::table {
|
||||||
sol::state_view lua = context.sol();
|
sol::state_view lua = context.sol();
|
||||||
sol::table providedServices(lua, sol::create);
|
sol::table providedServices(lua, sol::create);
|
||||||
constexpr std::array<std::pair<int, std::string_view>, 19> serviceNames = { { { ESM::NPC::Spells,
|
|
||||||
"Spells" },
|
|
||||||
{ ESM::NPC::Spellmaking, "Spellmaking" }, { ESM::NPC::Enchanting, "Enchanting" },
|
|
||||||
{ ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, { ESM::NPC::AllItems, "Barter" },
|
|
||||||
{ ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, { ESM::NPC::Clothing, "Clothing" },
|
|
||||||
{ ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, { ESM::NPC::Picks, "Picks" },
|
|
||||||
{ ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, { ESM::NPC::Apparatus, "Apparatus" },
|
|
||||||
{ ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, { ESM::NPC::Potions, "Potions" },
|
|
||||||
{ ESM::NPC::MagicItems, "MagicItems" } } };
|
|
||||||
|
|
||||||
int services = rec.mAiData.mServices;
|
int services = rec.mAiData.mServices;
|
||||||
if constexpr (std::is_same_v<T, ESM::NPC>)
|
if constexpr (std::is_same_v<T, ESM::NPC>)
|
||||||
|
@ -42,10 +34,11 @@ namespace MWLua
|
||||||
services
|
services
|
||||||
= MWBase::Environment::get().getESMStore()->get<ESM::Class>().find(rec.mClass)->mData.mServices;
|
= MWBase::Environment::get().getESMStore()->get<ESM::Class>().find(rec.mClass)->mData.mServices;
|
||||||
}
|
}
|
||||||
for (const auto& [flag, name] : serviceNames)
|
for (const auto& [flag, name] : MWLua::ServiceNames)
|
||||||
{
|
{
|
||||||
providedServices[name] = (services & flag) != 0;
|
providedServices[name] = (services & flag) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
providedServices["Travel"] = !rec.getTransport().empty();
|
providedServices["Travel"] = !rec.getTransport().empty();
|
||||||
return LuaUtil::makeReadOnly(providedServices);
|
return LuaUtil::makeReadOnly(providedServices);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "actor.hpp"
|
#include "actor.hpp"
|
||||||
#include "modelproperty.hpp"
|
#include "modelproperty.hpp"
|
||||||
|
#include "servicesoffered.hpp"
|
||||||
|
|
||||||
#include <components/esm3/loadfact.hpp>
|
#include <components/esm3/loadfact.hpp>
|
||||||
#include <components/esm3/loadnpc.hpp>
|
#include <components/esm3/loadnpc.hpp>
|
||||||
|
@ -44,6 +45,126 @@ namespace
|
||||||
|
|
||||||
return faction->mRanks.size();
|
return faction->mRanks.size();
|
||||||
}
|
}
|
||||||
|
ESM::NPC tableToNPC(const sol::table& rec)
|
||||||
|
{
|
||||||
|
ESM::NPC npc;
|
||||||
|
|
||||||
|
// Start from template if provided
|
||||||
|
if (rec["template"] != sol::nil)
|
||||||
|
npc = LuaUtil::cast<ESM::NPC>(rec["template"]);
|
||||||
|
else
|
||||||
|
npc.blank();
|
||||||
|
|
||||||
|
npc.mId = {};
|
||||||
|
|
||||||
|
// Basic fields
|
||||||
|
if (rec["name"] != sol::nil)
|
||||||
|
npc.mName = rec["name"];
|
||||||
|
if (rec["model"] != sol::nil)
|
||||||
|
npc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get<std::string_view>());
|
||||||
|
if (rec["mwscript"] != sol::nil)
|
||||||
|
npc.mScript = ESM::RefId::deserializeText(rec["mwscript"].get<std::string_view>());
|
||||||
|
if (rec["race"] != sol::nil)
|
||||||
|
npc.mRace = ESM::RefId::deserializeText(rec["race"].get<std::string_view>());
|
||||||
|
if (rec["class"] != sol::nil)
|
||||||
|
npc.mClass = ESM::RefId::deserializeText(rec["class"].get<std::string_view>());
|
||||||
|
if (rec["head"] != sol::nil)
|
||||||
|
npc.mHead = ESM::RefId::deserializeText(rec["head"].get<std::string_view>());
|
||||||
|
if (rec["hair"] != sol::nil)
|
||||||
|
npc.mHair = ESM::RefId::deserializeText(rec["hair"].get<std::string_view>());
|
||||||
|
if (rec["primaryFaction"] != sol::nil)
|
||||||
|
{
|
||||||
|
auto factionStr = rec["primaryFaction"].get<std::string_view>();
|
||||||
|
ESM::RefId factionId = ESM::RefId::deserializeText(factionStr);
|
||||||
|
|
||||||
|
const auto& factionStore = MWBase::Environment::get().getESMStore()->get<ESM::Faction>();
|
||||||
|
if (!factionStore.search(factionId))
|
||||||
|
throw std::runtime_error("Invalid faction '" + std::string(factionStr) + "' in primaryFaction");
|
||||||
|
|
||||||
|
npc.mFaction = factionId;
|
||||||
|
}
|
||||||
|
if (rec["isMale"] != sol::nil)
|
||||||
|
{
|
||||||
|
bool male = rec["isMale"];
|
||||||
|
if (male)
|
||||||
|
npc.mFlags &= ~ESM::NPC::Female;
|
||||||
|
else
|
||||||
|
npc.mFlags |= ESM::NPC::Female;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec["isEssential"] != sol::nil)
|
||||||
|
{
|
||||||
|
bool essential = rec["isEssential"];
|
||||||
|
if (essential)
|
||||||
|
npc.mFlags |= ESM::NPC::Essential;
|
||||||
|
else
|
||||||
|
npc.mFlags &= ~ESM::NPC::Essential;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec["isAutocalc"] != sol::nil)
|
||||||
|
{
|
||||||
|
bool autoCalc = rec["isAutocalc"];
|
||||||
|
if (autoCalc)
|
||||||
|
npc.mFlags |= ESM::NPC::Autocalc;
|
||||||
|
else
|
||||||
|
npc.mFlags &= ~ESM::NPC::Autocalc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec["isRespawning"] != sol::nil)
|
||||||
|
{
|
||||||
|
bool respawn = rec["isRespawning"];
|
||||||
|
if (respawn)
|
||||||
|
npc.mFlags |= ESM::NPC::Respawn;
|
||||||
|
else
|
||||||
|
npc.mFlags &= ~ESM::NPC::Respawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec["baseDisposition"] != sol::nil)
|
||||||
|
npc.mNpdt.mDisposition = rec["baseDisposition"].get<int>();
|
||||||
|
|
||||||
|
if (rec["baseGold"] != sol::nil)
|
||||||
|
npc.mNpdt.mGold = rec["baseGold"].get<int>();
|
||||||
|
|
||||||
|
if (rec["bloodType"] != sol::nil)
|
||||||
|
npc.mBloodType = rec["bloodType"].get<int>();
|
||||||
|
|
||||||
|
if (rec["primaryFactionRank"] != sol::nil)
|
||||||
|
{
|
||||||
|
if (!npc.mFaction.empty())
|
||||||
|
{
|
||||||
|
const ESM::Faction* faction
|
||||||
|
= MWBase::Environment::get().getESMStore()->get<ESM::Faction>().find(npc.mFaction);
|
||||||
|
|
||||||
|
int luaValue = rec["primaryFactionRank"];
|
||||||
|
int rank = LuaUtil::fromLuaIndex(luaValue);
|
||||||
|
|
||||||
|
int maxRank = static_cast<int>(getValidRanksCount(faction));
|
||||||
|
|
||||||
|
if (rank < 0 || rank >= maxRank)
|
||||||
|
throw std::runtime_error("primaryFactionRank: Requested rank " + std::to_string(rank)
|
||||||
|
+ " is out of bounds for faction " + npc.mFaction.toDebugString());
|
||||||
|
|
||||||
|
npc.mNpdt.mRank = rank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec["servicesOffered"] != sol::nil)
|
||||||
|
{
|
||||||
|
const sol::table services = rec["servicesOffered"];
|
||||||
|
int flags = 0;
|
||||||
|
|
||||||
|
for (const auto& [mask, key] : MWLua::ServiceNames)
|
||||||
|
{
|
||||||
|
sol::object value = services[key];
|
||||||
|
if (value != sol::nil && value.as<bool>())
|
||||||
|
flags |= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
npc.mAiData.mServices = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
return npc;
|
||||||
|
}
|
||||||
|
|
||||||
ESM::RefId parseFactionId(std::string_view faction)
|
ESM::RefId parseFactionId(std::string_view faction)
|
||||||
{
|
{
|
||||||
|
@ -95,9 +216,18 @@ namespace MWLua
|
||||||
= sol::readonly_property([](const ESM::NPC& rec) -> int { return (int)rec.mNpdt.mDisposition; });
|
= sol::readonly_property([](const ESM::NPC& rec) -> int { return (int)rec.mNpdt.mDisposition; });
|
||||||
record["head"]
|
record["head"]
|
||||||
= sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); });
|
= sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); });
|
||||||
|
record["primaryFaction"] = sol::readonly_property(
|
||||||
|
[](const ESM::NPC& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mFaction); });
|
||||||
|
record["primaryFactionRank"] = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> int {
|
||||||
|
if (rec.mFaction.empty())
|
||||||
|
return 0;
|
||||||
|
return LuaUtil::toLuaIndex(rec.mNpdt.mRank);
|
||||||
|
});
|
||||||
addModelProperty(record);
|
addModelProperty(record);
|
||||||
record["isEssential"]
|
record["isEssential"]
|
||||||
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; });
|
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; });
|
||||||
|
record["isAutocalc"]
|
||||||
|
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Autocalc; });
|
||||||
record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); });
|
record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); });
|
||||||
record["isRespawning"]
|
record["isRespawning"]
|
||||||
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; });
|
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; });
|
||||||
|
@ -152,6 +282,7 @@ namespace MWLua
|
||||||
stats.setBaseDisposition(stats.getBaseDisposition() + value);
|
stats.setBaseDisposition(stats.getBaseDisposition() + value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
npc["createRecordDraft"] = tableToNPC;
|
||||||
npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t {
|
npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t {
|
||||||
const MWWorld::Ptr ptr = actor.ptr();
|
const MWWorld::Ptr ptr = actor.ptr();
|
||||||
ESM::RefId factionId = parseFactionId(faction);
|
ESM::RefId factionId = parseFactionId(faction);
|
||||||
|
|
21
apps/openmw/mwlua/types/servicesoffered.hpp
Normal file
21
apps/openmw/mwlua/types/servicesoffered.hpp
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#ifndef MWLUA_SERVICESOFFERED_HPP
|
||||||
|
#define MWLUA_SERVICESOFFERED_HPP
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <components/esm3/loadnpc.hpp>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace MWLua
|
||||||
|
{
|
||||||
|
|
||||||
|
inline constexpr std::array<std::pair<int, std::string_view>, 19> ServiceNames
|
||||||
|
= { { { ESM::NPC::Spells, "Spells" }, { ESM::NPC::Spellmaking, "Spellmaking" },
|
||||||
|
{ ESM::NPC::Enchanting, "Enchanting" }, { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" },
|
||||||
|
{ ESM::NPC::AllItems, "Barter" }, { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" },
|
||||||
|
{ ESM::NPC::Clothing, "Clothing" }, { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" },
|
||||||
|
{ ESM::NPC::Picks, "Picks" }, { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" },
|
||||||
|
{ ESM::NPC::Apparatus, "Apparatus" }, { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" },
|
||||||
|
{ ESM::NPC::Potions, "Potions" }, { ESM::NPC::MagicItems, "MagicItems" } } };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -7,6 +7,7 @@
|
||||||
#include <components/esm3/loadclot.hpp>
|
#include <components/esm3/loadclot.hpp>
|
||||||
#include <components/esm3/loadligh.hpp>
|
#include <components/esm3/loadligh.hpp>
|
||||||
#include <components/esm3/loadmisc.hpp>
|
#include <components/esm3/loadmisc.hpp>
|
||||||
|
#include <components/esm3/loadnpc.hpp>
|
||||||
#include <components/esm3/loadweap.hpp>
|
#include <components/esm3/loadweap.hpp>
|
||||||
#include <components/lua/luastate.hpp>
|
#include <components/lua/luastate.hpp>
|
||||||
#include <components/misc/finitenumbers.hpp>
|
#include <components/misc/finitenumbers.hpp>
|
||||||
|
@ -188,6 +189,14 @@ namespace MWLua
|
||||||
checkGameInitialized(lua);
|
checkGameInitialized(lua);
|
||||||
return MWBase::Environment::get().getESMStore()->insert(potion);
|
return MWBase::Environment::get().getESMStore()->insert(potion);
|
||||||
},
|
},
|
||||||
|
[lua = context.mLua](const ESM::NPC& npc) -> const ESM::NPC* {
|
||||||
|
checkGameInitialized(lua);
|
||||||
|
if (npc.mId.empty())
|
||||||
|
return MWBase::Environment::get().getESMStore()->insert(npc);
|
||||||
|
ESM::NPC copy = npc;
|
||||||
|
copy.mId = {};
|
||||||
|
return MWBase::Environment::get().getESMStore()->insert(copy);
|
||||||
|
},
|
||||||
[lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* {
|
[lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* {
|
||||||
checkGameInitialized(lua);
|
checkGameInitialized(lua);
|
||||||
return MWBase::Environment::get().getESMStore()->insert(weapon);
|
return MWBase::Environment::get().getESMStore()->insert(weapon);
|
||||||
|
|
|
@ -726,8 +726,6 @@ namespace MWWorld
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ESM::REC_ALCH:
|
case ESM::REC_ALCH:
|
||||||
case ESM::REC_MISC:
|
|
||||||
case ESM::REC_ACTI:
|
|
||||||
case ESM::REC_ARMO:
|
case ESM::REC_ARMO:
|
||||||
case ESM::REC_BOOK:
|
case ESM::REC_BOOK:
|
||||||
case ESM::REC_CLAS:
|
case ESM::REC_CLAS:
|
||||||
|
@ -735,14 +733,16 @@ namespace MWWorld
|
||||||
case ESM::REC_ENCH:
|
case ESM::REC_ENCH:
|
||||||
case ESM::REC_SPEL:
|
case ESM::REC_SPEL:
|
||||||
case ESM::REC_WEAP:
|
case ESM::REC_WEAP:
|
||||||
case ESM::REC_LEVI:
|
|
||||||
case ESM::REC_LEVC:
|
|
||||||
case ESM::REC_LIGH:
|
|
||||||
mStoreImp->mRecNameToStore[type]->read(reader);
|
mStoreImp->mRecNameToStore[type]->read(reader);
|
||||||
return true;
|
return true;
|
||||||
case ESM::REC_NPC_:
|
case ESM::REC_NPC_:
|
||||||
case ESM::REC_CREA:
|
case ESM::REC_CREA:
|
||||||
case ESM::REC_CONT:
|
case ESM::REC_CONT:
|
||||||
|
case ESM::REC_MISC:
|
||||||
|
case ESM::REC_ACTI:
|
||||||
|
case ESM::REC_LEVI:
|
||||||
|
case ESM::REC_LEVC:
|
||||||
|
case ESM::REC_LIGH:
|
||||||
mStoreImp->mRecNameToStore[type]->read(reader, true);
|
mStoreImp->mRecNameToStore[type]->read(reader, true);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
|
@ -269,9 +269,12 @@ namespace MWWorld
|
||||||
list.push_back((*it)->mId);
|
list.push_back((*it)->mId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class T, class Id>
|
template <class T, class Id>
|
||||||
T* TypedDynamicStore<T, Id>::insert(const T& item, bool overrideOnly)
|
T* TypedDynamicStore<T, Id>::insert(const T& item, bool overrideOnly)
|
||||||
{
|
{
|
||||||
|
if constexpr (std::is_same_v<decltype(item.mId), ESM::RefId>)
|
||||||
|
overrideOnly = overrideOnly && !item.mId.template is<ESM::GeneratedRefId>();
|
||||||
if (overrideOnly)
|
if (overrideOnly)
|
||||||
{
|
{
|
||||||
auto it = mStatic.find(item.mId);
|
auto it = mStatic.find(item.mId);
|
||||||
|
|
|
@ -861,6 +861,9 @@
|
||||||
-- @field #boolean canWalk whether the creature can walk
|
-- @field #boolean canWalk whether the creature can walk
|
||||||
-- @field #boolean canUseWeapons whether the creature can use weapons and shields
|
-- @field #boolean canUseWeapons whether the creature can use weapons and shields
|
||||||
-- @field #boolean isBiped whether the creature is a biped
|
-- @field #boolean isBiped whether the creature is a biped
|
||||||
|
-- @field #boolean isAutocalc If true, the actors stats will be automatically calculated based on level and class.
|
||||||
|
-- @field #string primaryFaction Faction ID of the NPCs default faction. Nil if no faction
|
||||||
|
-- @field #number primaryFactionRank Faction rank of the NPCs default faction. Nil if no faction
|
||||||
-- @field #boolean isEssential whether the creature is essential
|
-- @field #boolean isEssential whether the creature is essential
|
||||||
-- @field #boolean isRespawning whether the creature respawns after death
|
-- @field #boolean isRespawning whether the creature respawns after death
|
||||||
-- @field #number bloodType integer representing the blood type of the Creature. Used to generate the correct blood vfx.
|
-- @field #number bloodType integer representing the blood type of the Creature. Used to generate the correct blood vfx.
|
||||||
|
@ -875,6 +878,13 @@
|
||||||
-- @field #Actor baseType @{#Actor}
|
-- @field #Actor baseType @{#Actor}
|
||||||
-- @field [parent=#NPC] #NpcStats stats
|
-- @field [parent=#NPC] #NpcStats stats
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Creates an @{#NpcRecord} without adding it to the world database.
|
||||||
|
-- Use @{openmw_world#(world).createRecord} to add the record to the world.
|
||||||
|
-- @function [parent=#NPC] createRecordDraft
|
||||||
|
-- @param #NpcRecord npc A Lua table with the fields of an NpcRecord, with an optional field `template` that accepts an @{#NpcRecord} as a base.
|
||||||
|
-- @return #NpcRecord A strongly typed NPC record.
|
||||||
|
|
||||||
---
|
---
|
||||||
-- A read-only list of all @{#NpcRecord}s in the world database, may be indexed by recordId.
|
-- A read-only list of all @{#NpcRecord}s in the world database, may be indexed by recordId.
|
||||||
-- Implements [iterables#List](iterables.html#List) of #NpcRecord.
|
-- Implements [iterables#List](iterables.html#List) of #NpcRecord.
|
||||||
|
|
Loading…
Reference in a new issue