mirror of
https://git.do.srb2.org/STJr/ZoneBuilder.git
synced 2025-02-07 08:21:10 +00:00
Look through SOC lumps for custom things and make them available in the editor (no sprites yet)
This commit is contained in:
parent
85225aaf1f
commit
ed578eab72
7 changed files with 486 additions and 11 deletions
|
@ -186,7 +186,9 @@
|
|||
<Compile Include="Map\Node.cs" />
|
||||
<Compile Include="Map\Seg.cs" />
|
||||
<Compile Include="Map\Subsector.cs" />
|
||||
<Compile Include="SRB2\SOCParser.cs" />
|
||||
<Compile Include="SRB2\SRB2Object.cs" />
|
||||
<Compile Include="SRB2\SOCObjectParser.cs" />
|
||||
<Compile Include="SRB2\LevelHeaderParser.cs" />
|
||||
<Compile Include="VisualModes\VisualBlockMap.cs" />
|
||||
<Compile Include="VisualModes\VisualMode.cs" />
|
||||
<Compile Include="General\Clock.cs" />
|
||||
|
|
|
@ -144,6 +144,7 @@ namespace CodeImp.DoomBuilder.Config
|
|||
this.angletext = parent.angletext;
|
||||
this.flagsvaluetext = parent.flagsvaluetext;
|
||||
this.parametertext = parent.parametertext;
|
||||
this.flags = parent.flags;
|
||||
}
|
||||
// Set default properties
|
||||
else
|
||||
|
@ -170,6 +171,7 @@ namespace CodeImp.DoomBuilder.Config
|
|||
this.angletext = "Angle";
|
||||
this.flagsvaluetext = "Flags value";
|
||||
this.parametertext = "Parameter";
|
||||
this.flags = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
// We have no destructor
|
||||
|
|
|
@ -22,6 +22,7 @@ using System.Globalization;
|
|||
using CodeImp.DoomBuilder.GZBuilder.Data;
|
||||
using CodeImp.DoomBuilder.IO;
|
||||
using CodeImp.DoomBuilder.Data;
|
||||
using CodeImp.DoomBuilder.SRB2;
|
||||
using CodeImp.DoomBuilder.ZDoom;
|
||||
using CodeImp.DoomBuilder.Map;
|
||||
using System.Drawing;
|
||||
|
@ -396,8 +397,55 @@ namespace CodeImp.DoomBuilder.Config
|
|||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
internal ThingTypeInfo(int index, ThingTypeInfo other)
|
||||
// Constructor
|
||||
internal ThingTypeInfo(ThingCategory cat, SRB2Object o)
|
||||
{
|
||||
// Initialize
|
||||
this.index = o.mapThingNum;
|
||||
this.category = cat;
|
||||
this.title = o.name;
|
||||
this.actor = null;
|
||||
this.classname = string.Empty; //mxd
|
||||
this.isknown = true;
|
||||
this.bright = false; //mxd
|
||||
this.args = new ArgumentInfo[Linedef.NUM_ARGS];
|
||||
for (int i = 0; i < Linedef.NUM_ARGS; i++) this.args[i] = new ArgumentInfo(i);
|
||||
|
||||
// Read properties
|
||||
this.sprite = cat.Sprite;
|
||||
this.color = cat.Color;
|
||||
this.alpha = cat.Alpha; //mxd
|
||||
this.alphabyte = (byte)(this.alpha * 255); //mxd
|
||||
this.renderstyle = cat.RenderStyle; //mxd
|
||||
this.arrow = (cat.Arrow != 0);
|
||||
this.radius = o.radius;
|
||||
this.height = o.height;
|
||||
this.hangs = (cat.Hangs != 0);
|
||||
this.blocking = cat.Blocking;
|
||||
this.errorcheck = cat.ErrorCheck;
|
||||
this.fixedsize = cat.FixedSize;
|
||||
this.fixedrotation = cat.FixedRotation; //mxd
|
||||
this.absolutez = cat.AbsoluteZ;
|
||||
this.spritescale = new SizeF(cat.SpriteScale, cat.SpriteScale);
|
||||
this.flags = new Dictionary<string, string>(cat.Flags);
|
||||
this.heightoffset = cat.HeightOffset;
|
||||
this.isUnflippable = cat.IsUnflippable;
|
||||
this.ignoreZ = cat.IgnoreZ;
|
||||
this.centerHitbox = cat.CenterHitbox;
|
||||
this.angletext = cat.AngleText;
|
||||
this.flagsvaluetext = cat.FlagsValueText;
|
||||
this.parametertext = cat.ParameterText;
|
||||
|
||||
// Safety
|
||||
if (this.radius < 4f) this.radius = 8f;
|
||||
if (this.hangs && this.absolutez) this.hangs = false; //mxd
|
||||
|
||||
// We have no destructor
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
internal ThingTypeInfo(int index, ThingTypeInfo other)
|
||||
{
|
||||
// Initialize
|
||||
this.index = index;
|
||||
|
|
|
@ -371,7 +371,7 @@ namespace CodeImp.DoomBuilder.Data
|
|||
Dictionary<int, string> spawnnums, doomednums;
|
||||
LoadMapInfo(out spawnnums, out doomednums);
|
||||
|
||||
int thingcount = LoadDecorateThings(spawnnums, doomednums);
|
||||
int thingcount = General.Map.SRB2 ? LoadCustomObjects() : LoadDecorateThings(spawnnums, doomednums);
|
||||
int spritecount = LoadThingSprites();
|
||||
LoadInternalSprites();
|
||||
LoadInternalTextures(); //mxd
|
||||
|
@ -526,7 +526,7 @@ namespace CodeImp.DoomBuilder.Data
|
|||
previews = null;
|
||||
|
||||
// Dispose decorate
|
||||
decorate.Dispose();
|
||||
if (decorate != null) decorate.Dispose();
|
||||
|
||||
// Dispose resources
|
||||
foreach(KeyValuePair<long, ImageData> i in textures) i.Value.Dispose();
|
||||
|
@ -1718,6 +1718,55 @@ namespace CodeImp.DoomBuilder.Data
|
|||
return counter;
|
||||
}
|
||||
|
||||
public int LoadCustomObjects()
|
||||
{
|
||||
SOCObjectParser parser = new SOCObjectParser { OnInclude = ParseFromLocation };
|
||||
|
||||
// Parse lumps
|
||||
foreach (DataReader dr in containers)
|
||||
{
|
||||
currentreader = dr;
|
||||
|
||||
Dictionary<string, Stream> streams1 = dr.GetObjctcfgData();
|
||||
Dictionary<string, Stream> streams2 = dr.GetMaincfgData();
|
||||
Dictionary<string, Stream> streams3 = dr.GetSOCData();
|
||||
Dictionary<string, Stream> streams = streams1.Concat(streams2).Concat(streams3).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
foreach (KeyValuePair<string, Stream> group in streams)
|
||||
{
|
||||
// Parse the data
|
||||
parser.Parse(group.Value, Path.Combine(dr.Location.location, group.Key), false);
|
||||
}
|
||||
}
|
||||
|
||||
currentreader = null;
|
||||
|
||||
if (!parser.HasError && parser.Objects.Count > 0)
|
||||
{
|
||||
ThingCategory cat = new ThingCategory(null, "customthings", "Custom Things");
|
||||
foreach (KeyValuePair<string,SRB2Object> o in parser.Objects)
|
||||
{
|
||||
ThingTypeInfo t = new ThingTypeInfo(cat, o.Value);
|
||||
cat.AddThing(t);
|
||||
// Check if we can find this thing in our existing collection
|
||||
if (thingtypes.ContainsKey(o.Value.mapThingNum))
|
||||
{
|
||||
// Update the thing
|
||||
thingtypes[o.Value.mapThingNum].Category.RemoveThing(thingtypes[o.Value.mapThingNum]);
|
||||
thingtypes[o.Value.mapThingNum] = t;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new thing
|
||||
thingtypes.Add(t.Index, t);
|
||||
}
|
||||
}
|
||||
thingcategories.Add(cat);
|
||||
|
||||
}
|
||||
return parser.Objects.Count;
|
||||
}
|
||||
|
||||
//mxd
|
||||
private static ThingCategory GetThingCategory(ThingCategory parent, List<ThingCategory> categories, string[] catnames)
|
||||
{
|
||||
|
@ -2096,7 +2145,7 @@ namespace CodeImp.DoomBuilder.Data
|
|||
{
|
||||
if (General.Map.SRB2)
|
||||
{
|
||||
SOCParser parser = new SOCParser { OnInclude = ParseFromLocation };
|
||||
LevelHeaderParser parser = new LevelHeaderParser { OnInclude = ParseFromLocation };
|
||||
|
||||
// Parse mapinfo
|
||||
foreach (DataReader dr in containers)
|
||||
|
|
|
@ -13,11 +13,11 @@ using CodeImp.DoomBuilder.GZBuilder.Data;
|
|||
|
||||
namespace CodeImp.DoomBuilder.SRB2
|
||||
{
|
||||
internal sealed class SOCParser : ZDTextParser
|
||||
internal sealed class LevelHeaderParser : ZDTextParser
|
||||
{
|
||||
#region ================== Delegates
|
||||
|
||||
public delegate void IncludeDelegate(SOCParser parser, string includefile, bool clearerror);
|
||||
public delegate void IncludeDelegate(LevelHeaderParser parser, string includefile, bool clearerror);
|
||||
public IncludeDelegate OnInclude;
|
||||
|
||||
#endregion
|
||||
|
@ -40,7 +40,7 @@ namespace CodeImp.DoomBuilder.SRB2
|
|||
|
||||
#region ================== Constructor
|
||||
|
||||
public SOCParser()
|
||||
public LevelHeaderParser()
|
||||
{
|
||||
// Syntax
|
||||
whitespace = "\n \t\r\u00A0";
|
||||
|
@ -71,9 +71,9 @@ namespace CodeImp.DoomBuilder.SRB2
|
|||
|
||||
while (!streamreader.EndOfStream)
|
||||
{
|
||||
string line = streamreader.ReadLine();
|
||||
string line = RemoveComments(streamreader.ReadLine());
|
||||
linenumber++;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n") || line.StartsWith("#")) continue;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n")) continue;
|
||||
string[] tokens = line.Split(new char[] { ' ' });
|
||||
switch (tokens[0].ToUpperInvariant())
|
||||
{
|
||||
|
@ -109,6 +109,7 @@ namespace CodeImp.DoomBuilder.SRB2
|
|||
linenumber++;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n")) break;
|
||||
if (line.StartsWith("#")) continue;
|
||||
line = RemoveComments(line);
|
||||
string[] tokens = line.Split(new char[] { '=' });
|
||||
if (tokens.Length != 2)
|
||||
{
|
||||
|
@ -206,6 +207,12 @@ namespace CodeImp.DoomBuilder.SRB2
|
|||
return "SOC";
|
||||
}
|
||||
|
||||
private string RemoveComments(string line)
|
||||
{
|
||||
string[] tokens = line.Split(new char[] { '#' });
|
||||
return tokens[0];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
310
Source/Core/SRB2/SOCObjectParser.cs
Normal file
310
Source/Core/SRB2/SOCObjectParser.cs
Normal file
|
@ -0,0 +1,310 @@
|
|||
#region ================== Namespaces
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using CodeImp.DoomBuilder.Compilers;
|
||||
using CodeImp.DoomBuilder.ZDoom;
|
||||
using CodeImp.DoomBuilder.GZBuilder.Data;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace CodeImp.DoomBuilder.SRB2
|
||||
{
|
||||
internal sealed class SOCObjectParser : ZDTextParser
|
||||
{
|
||||
#region ================== Delegates
|
||||
|
||||
public delegate void IncludeDelegate(SOCObjectParser parser, string includefile, bool clearerror);
|
||||
public IncludeDelegate OnInclude;
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Variables
|
||||
|
||||
private Dictionary<string, SRB2Object> objects;
|
||||
/*private Dictionary<string, SRB2State> states;
|
||||
private List<string> objectfreeslots;
|
||||
private List<string> statefreeslots;
|
||||
private List<string> spritefreeslots;*/
|
||||
private StreamReader streamreader;
|
||||
private int linenumber;
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Properties
|
||||
|
||||
public Dictionary<string, SRB2Object> Objects { get { return objects; } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Constructor
|
||||
|
||||
public SOCObjectParser()
|
||||
{
|
||||
// Syntax
|
||||
whitespace = "\n \t\r\u00A0";
|
||||
specialtokens = "=\n";
|
||||
|
||||
objects = new Dictionary<string,SRB2Object>();
|
||||
/*states = new Dictionary<string,SRB2State>();
|
||||
objectfreeslots = new List<string>();
|
||||
statefreeslots = new List<string>();
|
||||
spritefreeslots = new List<string>();*/
|
||||
}
|
||||
|
||||
// Disposer
|
||||
public void Dispose()
|
||||
{
|
||||
objects = null;
|
||||
/*states = null;
|
||||
objectfreeslots = null;
|
||||
statefreeslots = null;
|
||||
spritefreeslots = null;*/
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Parsing
|
||||
|
||||
override public bool Parse(Stream stream, string sourcefilename, bool clearerrors)
|
||||
{
|
||||
if (!base.Parse(stream, sourcefilename, clearerrors)) return false;
|
||||
|
||||
// Keep local data
|
||||
streamreader = new StreamReader(stream, Encoding.ASCII);
|
||||
linenumber = -1;
|
||||
|
||||
while (!streamreader.EndOfStream)
|
||||
{
|
||||
string line = RemoveComments(streamreader.ReadLine());
|
||||
linenumber++;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n")) continue;
|
||||
string[] tokens = line.Split(new char[] { ' ' });
|
||||
switch (tokens[0].ToUpperInvariant())
|
||||
{
|
||||
/*case "FREESLOT":
|
||||
if (!ParseFreeslots()) return false;
|
||||
break;*/
|
||||
case "OBJECT":
|
||||
case "MOBJ":
|
||||
case "THING":
|
||||
if (tokens.Length < 2 || String.IsNullOrEmpty(tokens[1]))
|
||||
{
|
||||
ReportError("Object block is missing an object name");
|
||||
break;
|
||||
}
|
||||
if (!ParseObject(tokens[1].ToUpperInvariant())) return false;
|
||||
break;
|
||||
/*case "STATE":
|
||||
case "FRAME":
|
||||
if (tokens.Length < 2 || String.IsNullOrEmpty(tokens[1]))
|
||||
{
|
||||
ReportError("State block is missing an state name");
|
||||
break;
|
||||
}
|
||||
if (!ParseState(tokens[1].ToUpperInvariant())) return false;
|
||||
break;*/
|
||||
}
|
||||
}
|
||||
|
||||
// All done
|
||||
return !this.HasError;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Map block parsing
|
||||
|
||||
/*private bool ParseFreeslots()
|
||||
{
|
||||
while (!streamreader.EndOfStream)
|
||||
{
|
||||
string line = streamreader.ReadLine();
|
||||
linenumber++;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n")) break;
|
||||
if (line.StartsWith("#")) continue;
|
||||
line = RemoveComments(line).Trim();
|
||||
if (line.StartsWith("MT_")) objectfreeslots.Add(line);
|
||||
else if (line.StartsWith("S_")) statefreeslots.Add(line);
|
||||
else if (line.StartsWith("SPR_")) spritefreeslots.Add(line);
|
||||
}
|
||||
return true;
|
||||
}*/
|
||||
|
||||
private bool ParseObject(string name)
|
||||
{
|
||||
if (name == null) return false;
|
||||
string[] states = new string[8];
|
||||
int mapThingNum = -1;
|
||||
int radius = 0;
|
||||
int height = 0;
|
||||
while (!streamreader.EndOfStream)
|
||||
{
|
||||
string line = streamreader.ReadLine();
|
||||
linenumber++;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n")) break;
|
||||
if (line.StartsWith("#")) continue;
|
||||
line = RemoveComments(line);
|
||||
string[] tokens = line.Split(new char[] { '=' });
|
||||
if (tokens.Length != 2)
|
||||
{
|
||||
ReportError("Invalid line");
|
||||
return false;
|
||||
}
|
||||
tokens[0] = tokens[0].Trim().ToUpperInvariant();
|
||||
tokens[1] = tokens[1].Trim().ToUpperInvariant();
|
||||
switch(tokens[0])
|
||||
{
|
||||
case "MAPTHINGNUM":
|
||||
if (!int.TryParse(tokens[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out mapThingNum))
|
||||
{
|
||||
ReportError("Invalid map thing number");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "RADIUS":
|
||||
if (!ParseWithArithmetic(tokens[1], out radius))
|
||||
{
|
||||
ReportError("Invalid radius");
|
||||
return false;
|
||||
}
|
||||
radius /= 65536;
|
||||
break;
|
||||
case "HEIGHT":
|
||||
if (!ParseWithArithmetic(tokens[1], out height))
|
||||
{
|
||||
ReportError("Invalid height");
|
||||
return false;
|
||||
}
|
||||
height /= 65536;
|
||||
break;
|
||||
|
||||
case "SPAWNSTATE":
|
||||
states[0] = tokens[1];
|
||||
break;
|
||||
case "SEESTATE":
|
||||
states[1] = tokens[1];
|
||||
break;
|
||||
case "PAINSTATE":
|
||||
states[2] = tokens[1];
|
||||
break;
|
||||
case "MELEESTATE":
|
||||
states[3] = tokens[1];
|
||||
break;
|
||||
case "MISSILESTATE":
|
||||
states[4] = tokens[1];
|
||||
break;
|
||||
case "DEATHSTATE":
|
||||
states[5] = tokens[1];
|
||||
break;
|
||||
case "XDEATHSTATE":
|
||||
states[6] = tokens[1];
|
||||
break;
|
||||
case "RAISESTATE":
|
||||
states[7] = tokens[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mapThingNum > 0)
|
||||
{
|
||||
SRB2Object o = new SRB2Object(name, states, mapThingNum, radius, height);
|
||||
if (objects.ContainsKey(name))
|
||||
objects[name] = o;
|
||||
else
|
||||
objects.Add(name, o);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*private bool ParseState(string name)
|
||||
{
|
||||
if (name == null) return false;
|
||||
string spritename = "";
|
||||
int spriteframe = 0;
|
||||
string next = "";
|
||||
while (!streamreader.EndOfStream)
|
||||
{
|
||||
string line = streamreader.ReadLine();
|
||||
linenumber++;
|
||||
if (String.IsNullOrEmpty(line) || line.StartsWith("\n")) break;
|
||||
if (line.StartsWith("#")) continue;
|
||||
line = RemoveComments(line);
|
||||
string[] tokens = line.Split(new char[] { '=' });
|
||||
if (tokens.Length != 2)
|
||||
{
|
||||
ReportError("Invalid line");
|
||||
return false;
|
||||
}
|
||||
tokens[0] = tokens[0].Trim().ToUpperInvariant();
|
||||
tokens[1] = tokens[1].Trim().ToUpperInvariant();
|
||||
switch (tokens[0])
|
||||
{
|
||||
case "SPRITENAME":
|
||||
case "SPRITENUMBER":
|
||||
spritename = tokens[1];
|
||||
break;
|
||||
case "SPRITEFRAME":
|
||||
case "SPRITESUBNUMBER":
|
||||
//TODO: Strip flags
|
||||
spriteframe = ParseSpriteFrame(tokens[1]);
|
||||
break;
|
||||
case "NEXT":
|
||||
next = tokens[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
states.Add(new SRB2State(name, spritename, spriteframe, next));
|
||||
|
||||
return true;
|
||||
}*/
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Methods
|
||||
|
||||
private bool ParseWithArithmetic(string input, out int output)
|
||||
{
|
||||
output = 1;
|
||||
string[] tokens = input.Split(new char[] { '*' });
|
||||
foreach (string t in tokens)
|
||||
{
|
||||
string trimmed = t.Trim();
|
||||
int val = 1;
|
||||
if (trimmed == "FRACUNIT") val = 65536;
|
||||
else if (!int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
|
||||
{
|
||||
ReportError("Invalid radius");
|
||||
return false;
|
||||
}
|
||||
output *= val;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This reports an error
|
||||
protected internal override void ReportError(string message)
|
||||
{
|
||||
// Set error information
|
||||
errordesc = message;
|
||||
errorline = (streamreader != null ? linenumber : CompilerError.NO_LINE_NUMBER); //mxd
|
||||
errorsource = sourcename;
|
||||
}
|
||||
|
||||
private string RemoveComments(string line)
|
||||
{
|
||||
string[] tokens = line.Split(new char[] { '#' });
|
||||
return tokens[0];
|
||||
}
|
||||
|
||||
protected override string GetLanguageType()
|
||||
{
|
||||
return "SOC";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
57
Source/Core/SRB2/SRB2Object.cs
Normal file
57
Source/Core/SRB2/SRB2Object.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
|
||||
#region ================== Copyright (c) 2007 Pascal vd Heiden
|
||||
|
||||
/*
|
||||
* Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
|
||||
* This program is released under GNU General Public License
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*/
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Namespaces
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace CodeImp.DoomBuilder.SRB2
|
||||
{
|
||||
public struct SRB2Object
|
||||
{
|
||||
#region ================== Constants
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Variables
|
||||
|
||||
public readonly string name;
|
||||
public readonly string[] states;
|
||||
public readonly int mapThingNum;
|
||||
public readonly int radius;
|
||||
public readonly int height;
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Constructor / Disposer
|
||||
|
||||
// Constructor
|
||||
internal SRB2Object(string name, string[] states, int mapThingNum, int radius, int height)
|
||||
{
|
||||
this.name = name;
|
||||
this.states = states;
|
||||
this.mapThingNum = mapThingNum;
|
||||
this.radius = radius;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue