mirror of
https://github.com/godotengine/godot.git
synced 2025-01-22 10:32:54 -05:00
Only allow valid types in Decal, Light3D projector, PointLight2D texture and CSGMesh3D mesh
If an invalid type is supplied (which can still be done from a script), a warning is printed (along with a workaround for ViewportTexture). This also adds support for "negative" resource hints such as "Texture2D,-ViewportTexture" to exclude one or more subclasses from a class hint. Co-authored-by: Tomasz Chabora <kobewi4e@gmail.com>
This commit is contained in:
parent
2b7ea6223b
commit
7a04d85ec3
10 changed files with 97 additions and 21 deletions
|
@ -65,7 +65,7 @@ enum PropertyHint {
|
|||
PROPERTY_HINT_DIR, ///< a directory path must be passed
|
||||
PROPERTY_HINT_GLOBAL_FILE, ///< a file path must be passed, hint_text (optionally) is a filter "*.png,*.wav,*.doc,"
|
||||
PROPERTY_HINT_GLOBAL_DIR, ///< a directory path must be passed
|
||||
PROPERTY_HINT_RESOURCE_TYPE, ///< a resource object type
|
||||
PROPERTY_HINT_RESOURCE_TYPE, ///< a comma-separated resource object type, e.g. "NoiseTexture,GradientTexture2D". Subclasses can be excluded with a "-" prefix if placed *after* the base class, e.g. "Texture2D,-MeshTexture".
|
||||
PROPERTY_HINT_MULTILINE_TEXT, ///< used for string properties that can contain multiple lines
|
||||
PROPERTY_HINT_EXPRESSION, ///< used for string properties that can contain multiple lines
|
||||
PROPERTY_HINT_PLACEHOLDER_TEXT, ///< used to set a placeholder text for string properties
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
<description>
|
||||
Returns an [Image] that is a copy of data from this [Texture2D] (a new [Image] is created each time). [Image]s can be accessed and manipulated directly.
|
||||
[b]Note:[/b] This will return [code]null[/code] if this [Texture2D] is invalid.
|
||||
[b]Note:[/b] This will fetch the texture data from the GPU, which might cause performance problems when overused.
|
||||
[b]Note:[/b] This will fetch the texture data from the GPU, which might cause performance problems when overused. Avoid calling [method get_image] every frame, especially on large textures.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_size" qualifiers="const">
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
To get a [ViewportTexture] in code, use the [method Viewport.get_texture] method on the target viewport.
|
||||
[b]Note:[/b] A [ViewportTexture] is always local to its scene (see [member Resource.resource_local_to_scene]). If the scene root is not ready, it may return incorrect data (see [signal Node.ready]).
|
||||
[b]Note:[/b] Instantiating scenes containing a high-resolution [ViewportTexture] may cause noticeable stutter.
|
||||
[b]Note:[/b] When using a [Viewport] with [member Viewport.use_hdr_2d] set to [code]true[/code] the returned texture will be an HDR image encoded in linear space. This may look darker than normal when displayed directly on screen. To convert to gamma space, you can do the following:
|
||||
[b]Note:[/b] When using a [Viewport] with [member Viewport.use_hdr_2d] set to [code]true[/code], the returned texture will be an HDR image encoded in linear space. This may look darker than normal when displayed directly on screen. To convert to gamma space, you can do the following:
|
||||
[codeblock]
|
||||
img.convert(Image.FORMAT_RGBA8)
|
||||
imb.linear_to_srgb()
|
||||
[/codeblock]
|
||||
[b]Note:[/b] Some nodes such as [Decal], [Light3D], and [PointLight2D] do not support using [ViewportTexture] directly. To use texture data from a [ViewportTexture] in these nodes, you need to create an [ImageTexture] by calling [method Texture2D.get_image] on the [ViewportTexture] and passing the result to [method ImageTexture.create_from_image]. This conversion is a slow operation, so it should not be performed every frame.
|
||||
</description>
|
||||
<tutorials>
|
||||
<link title="GUI in 3D Viewport Demo">https://godotengine.org/asset-library/asset/2807</link>
|
||||
|
|
|
@ -578,9 +578,9 @@ String EditorResourcePicker::_get_resource_type(const Ref<Resource> &p_resource)
|
|||
return res_type;
|
||||
}
|
||||
|
||||
static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_vector) {
|
||||
if (p_vector->has(p_type)) {
|
||||
// Already added
|
||||
static void _add_allowed_type(const StringName &p_type, List<StringName> *p_vector) {
|
||||
if (p_vector->find(p_type)) {
|
||||
// Already added.
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -588,7 +588,7 @@ static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_v
|
|||
// Engine class,
|
||||
|
||||
if (!ClassDB::is_virtual(p_type)) {
|
||||
p_vector->insert(p_type);
|
||||
p_vector->push_back(p_type);
|
||||
}
|
||||
|
||||
List<StringName> inheriters;
|
||||
|
@ -598,7 +598,7 @@ static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_v
|
|||
}
|
||||
} else {
|
||||
// Script class.
|
||||
p_vector->insert(p_type);
|
||||
p_vector->push_back(p_type);
|
||||
}
|
||||
|
||||
List<StringName> inheriters;
|
||||
|
@ -613,12 +613,22 @@ void EditorResourcePicker::_ensure_allowed_types() const {
|
|||
return;
|
||||
}
|
||||
|
||||
List<StringName> final_allowed;
|
||||
|
||||
Vector<String> allowed_types = base_type.split(",");
|
||||
int size = allowed_types.size();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
const String base = allowed_types[i].strip_edges();
|
||||
_add_allowed_type(base, &allowed_types_without_convert);
|
||||
for (const String &S : allowed_types) {
|
||||
const String base = S.strip_edges();
|
||||
if (base.begins_with("-")) {
|
||||
final_allowed.erase(base.right(-1));
|
||||
continue;
|
||||
}
|
||||
_add_allowed_type(base, &final_allowed);
|
||||
}
|
||||
|
||||
for (const StringName &SN : final_allowed) {
|
||||
allowed_types_without_convert.insert(SN);
|
||||
}
|
||||
|
||||
allowed_types_with_convert = HashSet<StringName>(allowed_types_without_convert);
|
||||
|
|
|
@ -243,3 +243,21 @@ GH-97449
|
|||
Validate extension JSON: Error: Field 'classes/GraphEdit/methods/connect_node/arguments': size changed value in new API, from 4 to 5.
|
||||
|
||||
Added optional argument to connect_node to specify whether the connection should be automatically deleted if invalid. Compatibility method registered.
|
||||
|
||||
|
||||
GH-88349
|
||||
--------
|
||||
Validate extension JSON: Error: Field 'classes/CSGMesh3D/properties/mesh': type changed value in new API, from "Mesh" to "Mesh,-PlaneMesh,-PointMesh,-QuadMesh,-RibbonTrailMesh".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/Light3D/properties/light_projector': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
Validate extension JSON: Error: Field 'classes/PointLight2D/properties/texture': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture".
|
||||
|
||||
Property hints modified to disallow resource types that don't work. The types allowed are now more restricted, but this change only impacts the editor and not the actual exposed API. No adjustments should be necessary.
|
||||
Decal properties were previously changed from Texture to Texture2D in 4.2, so we need to silence those warnings too.
|
||||
|
|
|
@ -1207,7 +1207,8 @@ void CSGMesh3D::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("set_material", "material"), &CSGMesh3D::set_material);
|
||||
ClassDB::bind_method(D_METHOD("get_material"), &CSGMesh3D::get_material);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "mesh", PROPERTY_HINT_RESOURCE_TYPE, "Mesh"), "set_mesh", "get_mesh");
|
||||
// Hide PrimitiveMeshes that are always non-manifold and therefore can't be used as CSG meshes.
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "mesh", PROPERTY_HINT_RESOURCE_TYPE, "Mesh,-PlaneMesh,-PointMesh,-QuadMesh,-RibbonTrailMesh"), "set_mesh", "get_mesh");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material", PROPERTY_HINT_RESOURCE_TYPE, "BaseMaterial3D,ShaderMaterial"), "set_material", "get_material");
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
A CSG Mesh shape that uses a mesh resource.
|
||||
</brief_description>
|
||||
<description>
|
||||
This CSG node allows you to use any mesh resource as a CSG shape, provided it is closed, does not self-intersect, does not contain internal faces and has no edges that connect to more than two faces. See also [CSGPolygon3D] for drawing 2D extruded polygons to be used as CSG nodes.
|
||||
This CSG node allows you to use any mesh resource as a CSG shape, provided it is [i]manifold[/i]. A manifold shape is closed, does not self-intersect, does not contain internal faces and has no edges that connect to more than two faces. See also [CSGPolygon3D] for drawing 2D extruded polygons to be used as CSG nodes.
|
||||
[b]Note:[/b] CSG nodes are intended to be used for level prototyping. Creating CSG nodes has a significant CPU cost compared to creating a [MeshInstance3D] with a [PrimitiveMesh]. Moving a CSG node within another CSG node also has a significant CPU cost, so it should be avoided during gameplay.
|
||||
</description>
|
||||
<tutorials>
|
||||
|
@ -16,6 +16,7 @@
|
|||
</member>
|
||||
<member name="mesh" type="Mesh" setter="set_mesh" getter="get_mesh">
|
||||
The [Mesh] resource to use as a CSG shape.
|
||||
[b]Note:[/b] Some [Mesh] types such as [PlaneMesh], [PointMesh], [QuadMesh], and [RibbonTrailMesh] are excluded from the type hint for this property, as these primitives are non-[i]manifold[/i] and thus not compatible with the CSG algorithm.
|
||||
[b]Note:[/b] When using an [ArrayMesh], all vertex attributes except [constant Mesh.ARRAY_VERTEX], [constant Mesh.ARRAY_NORMAL] and [constant Mesh.ARRAY_TEX_UV] are left unused. Only [constant Mesh.ARRAY_VERTEX] and [constant Mesh.ARRAY_TEX_UV] will be passed to the GPU.
|
||||
[constant Mesh.ARRAY_NORMAL] is only used to determine which faces require the use of flat shading. By default, CSGMesh will ignore the mesh's vertex normals, recalculate them for each vertex and use a smooth shader. If a flat shader is required for a face, ensure that all vertex normals of the face are approximately equal.
|
||||
</member>
|
||||
|
|
|
@ -395,6 +395,19 @@ Rect2 PointLight2D::get_anchorable_rect() const {
|
|||
void PointLight2D::set_texture(const Ref<Texture2D> &p_texture) {
|
||||
texture = p_texture;
|
||||
if (texture.is_valid()) {
|
||||
#ifdef DEBUG_ENABLED
|
||||
if (
|
||||
p_texture->is_class("AnimatedTexture") ||
|
||||
p_texture->is_class("AtlasTexture") ||
|
||||
p_texture->is_class("CameraTexture") ||
|
||||
p_texture->is_class("CanvasTexture") ||
|
||||
p_texture->is_class("MeshTexture") ||
|
||||
p_texture->is_class("Texture2DRD") ||
|
||||
p_texture->is_class("ViewportTexture")) {
|
||||
WARN_PRINT(vformat("%s cannot be used as a PointLight2D texture (%s). As a workaround, assign the value returned by %s's `get_image()` instead.", p_texture->get_class(), get_path(), p_texture->get_class()));
|
||||
}
|
||||
#endif
|
||||
|
||||
RS::get_singleton()->canvas_light_set_texture(_get_light(), texture->get_rid());
|
||||
} else {
|
||||
RS::get_singleton()->canvas_light_set_texture(_get_light(), RID());
|
||||
|
@ -462,7 +475,8 @@ void PointLight2D::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("set_texture_scale", "texture_scale"), &PointLight2D::set_texture_scale);
|
||||
ClassDB::bind_method(D_METHOD("get_texture_scale"), &PointLight2D::get_texture_scale);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture");
|
||||
// Only allow texture types that display correctly.
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture"), "set_texture", "get_texture");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset", PROPERTY_HINT_NONE, "suffix:px"), "set_texture_offset", "get_texture_offset");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "texture_scale", PROPERTY_HINT_RANGE, "0.01,50,0.01"), "set_texture_scale", "get_texture_scale");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height", PROPERTY_HINT_RANGE, "0,1024,1,or_greater,suffix:px"), "set_height", "get_height");
|
||||
|
|
|
@ -44,6 +44,20 @@ void Decal::set_texture(DecalTexture p_type, const Ref<Texture2D> &p_texture) {
|
|||
ERR_FAIL_INDEX(p_type, TEXTURE_MAX);
|
||||
textures[p_type] = p_texture;
|
||||
RID texture_rid = p_texture.is_valid() ? p_texture->get_rid() : RID();
|
||||
|
||||
#ifdef DEBUG_ENABLED
|
||||
if (
|
||||
p_texture->is_class("AnimatedTexture") ||
|
||||
p_texture->is_class("AtlasTexture") ||
|
||||
p_texture->is_class("CameraTexture") ||
|
||||
p_texture->is_class("CanvasTexture") ||
|
||||
p_texture->is_class("MeshTexture") ||
|
||||
p_texture->is_class("Texture2DRD") ||
|
||||
p_texture->is_class("ViewportTexture")) {
|
||||
WARN_PRINT(vformat("%s cannot be used as a Decal texture (%s). As a workaround, assign the value returned by %s's `get_image()` instead.", p_texture->get_class(), get_path(), p_texture->get_class()));
|
||||
}
|
||||
#endif
|
||||
|
||||
RS::get_singleton()->decal_set_texture(decal, RS::DecalTexture(p_type), texture_rid);
|
||||
update_configuration_warnings();
|
||||
}
|
||||
|
@ -225,10 +239,12 @@ void Decal::_bind_methods() {
|
|||
ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "size", PROPERTY_HINT_RANGE, "0,1024,0.001,or_greater,suffix:m"), "set_size", "get_size");
|
||||
|
||||
ADD_GROUP("Textures", "texture_");
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_albedo", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture", TEXTURE_ALBEDO);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_normal", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture", TEXTURE_NORMAL);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_orm", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture", TEXTURE_ORM);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_emission", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture", TEXTURE_EMISSION);
|
||||
// Only allow texture types that display correctly.
|
||||
const String texture_hint = "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture";
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_albedo", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_ALBEDO);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_normal", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_NORMAL);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_orm", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_ORM);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_emission", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_EMISSION);
|
||||
|
||||
ADD_GROUP("Parameters", "");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_energy", PROPERTY_HINT_RANGE, "0,16,0.01,or_greater"), "set_emission_energy", "get_emission_energy");
|
||||
|
|
|
@ -28,10 +28,10 @@
|
|||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "core/config/project_settings.h"
|
||||
|
||||
#include "light_3d.h"
|
||||
|
||||
#include "core/config/project_settings.h"
|
||||
|
||||
void Light3D::set_param(Param p_param, real_t p_value) {
|
||||
ERR_FAIL_INDEX(p_param, PARAM_MAX);
|
||||
param[p_param] = p_value;
|
||||
|
@ -200,6 +200,20 @@ Light3D::BakeMode Light3D::get_bake_mode() const {
|
|||
void Light3D::set_projector(const Ref<Texture2D> &p_texture) {
|
||||
projector = p_texture;
|
||||
RID tex_id = projector.is_valid() ? projector->get_rid() : RID();
|
||||
|
||||
#ifdef DEBUG_ENABLED
|
||||
if (
|
||||
p_texture->is_class("AnimatedTexture") ||
|
||||
p_texture->is_class("AtlasTexture") ||
|
||||
p_texture->is_class("CameraTexture") ||
|
||||
p_texture->is_class("CanvasTexture") ||
|
||||
p_texture->is_class("MeshTexture") ||
|
||||
p_texture->is_class("Texture2DRD") ||
|
||||
p_texture->is_class("ViewportTexture")) {
|
||||
WARN_PRINT(vformat("%s cannot be used as a Light3D projector texture (%s). As a workaround, assign the value returned by %s's `get_image()` instead.", p_texture->get_class(), get_path(), p_texture->get_class()));
|
||||
}
|
||||
#endif
|
||||
|
||||
RS::get_singleton()->light_set_projector(light, tex_id);
|
||||
update_configuration_warnings();
|
||||
}
|
||||
|
@ -384,7 +398,8 @@ void Light3D::_bind_methods() {
|
|||
ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "light_energy", PROPERTY_HINT_RANGE, "0,16,0.001,or_greater"), "set_param", "get_param", PARAM_ENERGY);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "light_indirect_energy", PROPERTY_HINT_RANGE, "0,16,0.001,or_greater"), "set_param", "get_param", PARAM_INDIRECT_ENERGY);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "light_volumetric_fog_energy", PROPERTY_HINT_RANGE, "0,16,0.001,or_greater"), "set_param", "get_param", PARAM_VOLUMETRIC_FOG_ENERGY);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "light_projector", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_projector", "get_projector");
|
||||
// Only allow texture types that display correctly.
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "light_projector", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture"), "set_projector", "get_projector");
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "light_size", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"), "set_param", "get_param", PARAM_SIZE);
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "light_angular_distance", PROPERTY_HINT_RANGE, "0,90,0.01,degrees"), "set_param", "get_param", PARAM_SIZE);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "light_negative"), "set_negative", "is_negative");
|
||||
|
|
Loading…
Reference in a new issue