Classic modes: further text label rendering optimization.

MODELDEF parser: rewrote most of the parser logic. Now it picks actor model(s) based on Frame / FrameName properties.
This commit is contained in:
MaxED 2016-04-06 11:44:38 +00:00
parent ee12da96a1
commit 580f7d4461
10 changed files with 605 additions and 572 deletions

View file

@ -1,34 +1,50 @@
using System; #region ================== Namespaces
using System;
using System.Collections.Generic; using System.Collections.Generic;
using CodeImp.DoomBuilder.Config; using CodeImp.DoomBuilder.Config;
using CodeImp.DoomBuilder.Data; using CodeImp.DoomBuilder.Data;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.ZDoom; using CodeImp.DoomBuilder.ZDoom;
using CodeImp.DoomBuilder.GZBuilder.Data; using CodeImp.DoomBuilder.GZBuilder.Data;
using SlimDX;
#endregion
namespace CodeImp.DoomBuilder.GZBuilder.GZDoom namespace CodeImp.DoomBuilder.GZBuilder.GZDoom
{ {
internal class ModeldefParser : ZDTextParser internal class ModeldefParser : ZDTextParser
{ {
internal override ScriptType ScriptType { get { return ScriptType.MODELDEF; } } #region ================== Variables
private readonly Dictionary<string, int> actorsbyclass; private readonly Dictionary<string, int> actorsbyclass;
internal Dictionary<string, int> ActorsByClass { get { return actorsbyclass; } }
private Dictionary<string, ModelData> entries; //classname, entry private Dictionary<string, ModelData> entries; //classname, entry
#endregion
#region ================== Properties
internal override ScriptType ScriptType { get { return ScriptType.MODELDEF; } }
internal Dictionary<string, ModelData> Entries { get { return entries; } } internal Dictionary<string, ModelData> Entries { get { return entries; } }
#endregion
#region ================== Constructor
internal ModeldefParser(Dictionary<string, int> actorsbyclass) internal ModeldefParser(Dictionary<string, int> actorsbyclass)
{ {
this.actorsbyclass = actorsbyclass; this.actorsbyclass = actorsbyclass;
this.entries = new Dictionary<string, ModelData>(StringComparer.Ordinal); this.entries = new Dictionary<string, ModelData>(StringComparer.OrdinalIgnoreCase);
} }
//should be called after all decorate actors are parsed #endregion
#region ================== Parsing
// Should be called after all decorate actors are parsed
public override bool Parse(TextResourceData data, bool clearerrors) public override bool Parse(TextResourceData data, bool clearerrors)
{ {
entries = new Dictionary<string, ModelData>(StringComparer.Ordinal); // Already parsed?
//mxd. Already parsed?
if(!base.AddTextResource(data)) if(!base.AddTextResource(data))
{ {
if(clearerrors) ClearError(); if(clearerrors) ClearError();
@ -42,72 +58,75 @@ namespace CodeImp.DoomBuilder.GZBuilder.GZDoom
while(SkipWhitespace(true)) while(SkipWhitespace(true))
{ {
string token = ReadToken(); string token = ReadToken();
if(!string.IsNullOrEmpty(token)) if(string.IsNullOrEmpty(token)) continue;
token = StripTokenQuotes(token).ToLowerInvariant();
if(token != "model") continue;
// Find classname
SkipWhitespace(true);
string classname = ReadToken(ActorStructure.ACTOR_CLASS_SPECIAL_TOKENS);
if(string.IsNullOrEmpty(classname))
{ {
token = StripTokenQuotes(token).ToLowerInvariant(); ReportError("Expected actor class");
if(token == "model") //model structure start return false;
}
// Now find opening brace
if(!NextTokenIs("{")) return false;
// Parse the structure
ModeldefStructure mds = new ModeldefStructure();
if(mds.Parse(this))
{
// Fetch Actor info
if(actorsbyclass.ContainsKey(classname))
{ {
// Find classname ThingTypeInfo info = General.Map.Data.GetThingInfoEx(actorsbyclass[classname]);
SkipWhitespace(true);
string displayclassname = StripTokenQuotes(ReadToken(ActorStructure.ACTOR_CLASS_SPECIAL_TOKENS));
string classname = displayclassname.ToLowerInvariant();
if(!string.IsNullOrEmpty(classname) && !entries.ContainsKey(classname)) // Actor has a valid sprite?
if(info != null && !string.IsNullOrEmpty(info.Sprite) && !info.Sprite.ToLowerInvariant().StartsWith(DataManager.INTERNAL_PREFIX))
{ {
// Now find opening brace string targetsprite = info.Sprite.Substring(0, 5);
if(!NextTokenIs("{")) return false; if(mds.Frames.ContainsKey(targetsprite))
ModeldefStructure mds = new ModeldefStructure();
if(mds.Parse(this, displayclassname) && mds.ModelData != null)
{ {
entries.Add(classname, mds.ModelData); // Create model data
} ModelData md = new ModelData { InheritActorPitch = mds.InheritActorPitch, InheritActorRoll = mds.InheritActorRoll };
if(HasError) // Things are complicated in GZDoom...
{ Matrix moffset = Matrix.Translation(mds.Offset.Y, -mds.Offset.X, mds.Offset.Z);
LogError(); Matrix mrotation = Matrix.RotationY(-Angle2D.DegToRad(mds.RollOffset)) * Matrix.RotationX(-Angle2D.DegToRad(mds.PitchOffset)) * Matrix.RotationZ(Angle2D.DegToRad(mds.AngleOffset));
ClearError(); md.SetTransform(mrotation, moffset, mds.Scale);
}
// Skip untill current structure end // Add models
if(!mds.ParsingFinished) SkipStructure(1); foreach(var fs in mds.Frames[targetsprite])
{
// Texture name will be empty when skin path is embedded in the model
string texturename = (!string.IsNullOrEmpty(mds.TextureNames[fs.ModelIndex]) ? mds.TextureNames[fs.ModelIndex].ToLowerInvariant() : string.Empty);
md.TextureNames.Add(texturename);
md.ModelNames.Add(mds.ModelNames[fs.ModelIndex].ToLowerInvariant());
md.FrameNames.Add(fs.FrameName);
md.FrameIndices.Add(fs.FrameIndex);
}
// Add to collection
entries[classname] = md;
}
} }
} }
else }
{
// Unknown structure!
if(token != "{")
{
string token2;
do
{
if(!SkipWhitespace(true)) break;
token2 = ReadToken();
if(string.IsNullOrEmpty(token2)) break;
}
while(token2 != "{");
}
SkipStructure(1); if(HasError)
} {
LogError();
ClearError();
} }
} }
return entries.Count > 0; return true;
} }
// Skips untill current structure end #endregion
private void SkipStructure(int scopelevel)
{
do
{
if(!SkipWhitespace(true)) break;
string token = ReadToken();
if(string.IsNullOrEmpty(token)) break;
if(token == "{") scopelevel++;
if(token == "}") scopelevel--;
}
while(scopelevel > 0);
}
} }
} }

View file

@ -1,11 +1,11 @@
#region ================== Namespaces #region ================== Namespaces
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Globalization; using System.Globalization;
using SlimDX; using SlimDX;
using CodeImp.DoomBuilder.GZBuilder.Data; using CodeImp.DoomBuilder.GZBuilder.Data;
using CodeImp.DoomBuilder.Geometry;
#endregion #endregion
@ -13,478 +13,482 @@ namespace CodeImp.DoomBuilder.GZBuilder.GZDoom
{ {
internal sealed class ModeldefStructure internal sealed class ModeldefStructure
{ {
#region ================== Constants
private const int MAX_MODELS = 4; //maximum models per modeldef entry, zero-based private const int MAX_MODELS = 4; //maximum models per modeldef entry, zero-based
private bool parsingfinished;
internal ModelData ModelData; #endregion
internal bool ParsingFinished { get { return parsingfinished; } }
internal bool Parse(ModeldefParser parser, string classname) #region ================== Structs
internal struct FrameStructure
{ {
public string SpriteName; // Stays here for HashSet duplicate checks
public int ModelIndex;
public int FrameIndex;
public string FrameName;
}
#region ================== Vars #endregion
string[] textureNames = new string[MAX_MODELS]; #region ================== Variables
string[] modelNames = new string[MAX_MODELS];
string[] frameNames = new string[MAX_MODELS];
int[] frameIndices = new int[MAX_MODELS];
bool[] modelsUsed = new bool[MAX_MODELS];
string path = "";
Vector3 scale = new Vector3(1, 1, 1);
Vector3 offset = new Vector3();
float angleOffset = 0;
float pitchOffset = 0;
float rollOffset = 0;
bool inheritactorpitch = false;
bool inheritactorroll = false;
string token; private string[] texturenames;
private string[] modelnames;
private string path;
private Vector3 scale;
private Vector3 offset;
private float angleoffset;
private float pitchoffset;
private float rolloffset;
private bool inheritactorpitch;
private bool inheritactorroll;
#endregion private Dictionary<string, HashSet<FrameStructure>> frames;
//read modeldef structure contents #endregion
parsingfinished = false;
#region ================== Properties
public string[] TextureNames { get { return texturenames; } }
public string[] ModelNames { get { return modelnames; } }
public Vector3 Scale { get { return scale; } }
public Vector3 Offset { get { return offset; } }
public float AngleOffset { get { return angleoffset; } }
public float PitchOffset { get { return pitchoffset; } }
public float RollOffset { get { return rolloffset; } }
public bool InheritActorPitch { get { return inheritactorpitch; } }
public bool InheritActorRoll { get { return inheritactorroll; } }
public Dictionary<string, HashSet<FrameStructure>> Frames { get { return frames; } }
#endregion
#region ================== Constructor
internal ModeldefStructure()
{
texturenames = new string[MAX_MODELS];
modelnames = new string[MAX_MODELS];
frames = new Dictionary<string, HashSet<FrameStructure>>(StringComparer.OrdinalIgnoreCase);
scale = new Vector3(1.0f, 1.0f, 1.0f);
}
#endregion
#region ================== Parsing
internal bool Parse(ModeldefParser parser)
{
// Read modeldef structure contents
bool parsingfinished = false;
while(!parsingfinished && parser.SkipWhitespace(true)) while(!parsingfinished && parser.SkipWhitespace(true))
{ {
token = parser.ReadToken(); string token = parser.ReadToken();
if(!string.IsNullOrEmpty(token)) if(string.IsNullOrEmpty(token)) continue;
switch(token.ToLowerInvariant())
{ {
token = parser.StripTokenQuotes(token).ToLowerInvariant(); //ANYTHING can be quoted... case "path":
switch(token) parser.SkipWhitespace(true);
{ path = parser.StripTokenQuotes(parser.ReadToken(false)).Replace("/", "\\"); // Don't skip newline
if(string.IsNullOrEmpty(path))
#region ================== Path {
parser.ReportError("Expected model path");
case "path": return false;
parser.SkipWhitespace(true); }
path = parser.StripTokenQuotes(parser.ReadToken(false)).Replace("/", "\\"); // Don't skip newline break;
if(string.IsNullOrEmpty(path))
{ case "model":
parser.ReportError("Expected model path"); parser.SkipWhitespace(true);
return false;
} // Model index
break; int index;
token = parser.ReadToken();
#endregion if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out index) || index < 0)
{
#region ================== Model // Not numeric!
parser.ReportError("Expected model index, but got \"" + token + "\"");
case "model": return false;
parser.SkipWhitespace(true); }
//model index if(index >= MAX_MODELS)
int index; {
token = parser.StripTokenQuotes(parser.ReadToken()); parser.ReportError("GZDoom doesn't allow more than " + MAX_MODELS + " models per MODELDEF entry");
if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out index)) return false;
{ }
// Not numeric!
parser.ReportError("Expected model index, but got \"" + token + "\""); parser.SkipWhitespace(true);
return false;
} // Model path
token = parser.StripTokenQuotes(parser.ReadToken(false)).ToLowerInvariant(); // Don't skip newline
if(index >= MAX_MODELS) if(string.IsNullOrEmpty(token))
{ {
parser.ReportError("GZDoom doesn't allow more than " + MAX_MODELS + " models per MODELDEF entry"); parser.ReportError("Expected model name");
return false; return false;
} }
parser.SkipWhitespace(true); // Check invalid path chars
if(!parser.CheckInvalidPathChars(token)) return false;
//model path
token = parser.StripTokenQuotes(parser.ReadToken(false)).ToLowerInvariant(); // Don't skip newline // Check extension
if(string.IsNullOrEmpty(token)) string modelext = Path.GetExtension(token);
{ if(string.IsNullOrEmpty(modelext))
parser.ReportError("Expected model name"); {
return false; parser.ReportError("Model \"" + token + "\" won't be loaded. Models without extension are not supported by GZDoom");
} return false;
}
//check invalid path chars
if(!parser.CheckInvalidPathChars(token)) return false; if(modelext != ".md3" && modelext != ".md2")
{
//check extension parser.ReportError("Model \"" + token + "\" won't be loaded. Only MD2 and MD3 models are supported");
string modelext = Path.GetExtension(token); return false;
if(string.IsNullOrEmpty(modelext)) }
{
parser.ReportError("Model \"" + token + "\" won't be loaded. Models without extension are not supported by GZDoom"); // GZDoom allows models with identical index, it uses the last one encountered
return false; modelnames[index] = Path.Combine(path, token);
} break;
if(modelext != ".md3" && modelext != ".md2") case "skin":
{ parser.SkipWhitespace(true);
parser.ReportError("Model \"" + token + "\" won't be loaded. Only MD2 and MD3 models are supported");
return false; // Skin index
} int skinindex;
token = parser.ReadToken();
//GZDoom allows models with identical modelIndex, it uses the last one encountered if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out skinindex) || skinindex < 0)
modelNames[index] = Path.Combine(path, token); {
break; // Not numeric!
parser.ReportError("Expected skin index, but got \"" + token + "\"");
#endregion return false;
}
#region ================== Skin
if(skinindex >= MAX_MODELS)
case "skin": {
parser.SkipWhitespace(true); parser.ReportError("GZDoom doesn't allow more than " + MAX_MODELS + " skins per MODELDEF entry");
return false;
//skin index }
int skinIndex;
token = parser.StripTokenQuotes(parser.ReadToken()); parser.SkipWhitespace(true);
if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out skinIndex))
{ // Skin path
// Not numeric! token = parser.StripTokenQuotes(parser.ReadToken(false)).ToLowerInvariant(); // Don't skip newline
parser.ReportError("Expected skin index, but got \"" + token + "\""); if(string.IsNullOrEmpty(token))
return false; {
} parser.ReportError("Expected skin path");
return false;
if(skinIndex >= MAX_MODELS) }
{
parser.ReportError("GZDoom doesn't allow more than " + MAX_MODELS + " skins per MODELDEF entry"); // Check invalid path chars
return false; if(!parser.CheckInvalidPathChars(token)) return false;
}
// Check extension
parser.SkipWhitespace(true); string texext = Path.GetExtension(token);
if(Array.IndexOf(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, texext) == -1)
//skin path {
token = parser.StripTokenQuotes(parser.ReadToken(false)).ToLowerInvariant(); // Don't skip newline parser.ReportError("Image format \"" + texext + "\" is not supported");
if(string.IsNullOrEmpty(token)) return false;
{ }
parser.ReportError("Expected skin path");
return false; // GZDoom allows skins with identical index, it uses the last one encountered
} texturenames[skinindex] = Path.Combine(path, token);
break;
//check invalid path chars
if(!parser.CheckInvalidPathChars(token)) return false; case "scale":
parser.SkipWhitespace(true);
//check extension token = parser.ReadToken();
string texext = Path.GetExtension(token); if(!parser.ReadSignedFloat(token, ref scale.Y))
if(Array.IndexOf(ModelData.SUPPORTED_TEXTURE_EXTENSIONS, texext) == -1) {
{ // Not numeric!
parser.ReportError("Image format \"" + texext + "\" is not supported"); parser.ReportError("Expected Scale X value, but got \"" + token + "\"");
return false; return false;
} }
//GZDoom allows skins with identical modelIndex, it uses the last one encountered parser.SkipWhitespace(true);
textureNames[skinIndex] = Path.Combine(path, token); token = parser.ReadToken();
break; if(!parser.ReadSignedFloat(token, ref scale.X))
{
#endregion // Not numeric!
parser.ReportError("Expected Scale Y value, but got \"" + token + "\"");
#region ================== Scale return false;
}
case "scale":
parser.SkipWhitespace(true); parser.SkipWhitespace(true);
token = parser.StripTokenQuotes(parser.ReadToken()); token = parser.ReadToken();
if(!parser.ReadSignedFloat(token, ref scale.Y)) if(!parser.ReadSignedFloat(token, ref scale.Z))
{ {
// Not numeric! // Not numeric!
parser.ReportError("Expected Scale X value, but got \"" + token + "\""); parser.ReportError("Expected Scale Z value, but got \"" + token + "\"");
return false; return false;
} }
break;
parser.SkipWhitespace(true);
token = parser.StripTokenQuotes(parser.ReadToken()); case "offset":
if(!parser.ReadSignedFloat(token, ref scale.X)) parser.SkipWhitespace(true);
{ token = parser.ReadToken();
// Not numeric! if(!parser.ReadSignedFloat(token, ref offset.X))
parser.ReportError("Expected Scale Y value, but got \"" + token + "\""); {
return false; // Not numeric!
} parser.ReportError("Expected Offset X value, but got \"" + token + "\"");
return false;
parser.SkipWhitespace(true); }
token = parser.StripTokenQuotes(parser.ReadToken());
if(!parser.ReadSignedFloat(token, ref scale.Z)) parser.SkipWhitespace(true);
{ token = parser.ReadToken();
// Not numeric! if(!parser.ReadSignedFloat(token, ref offset.Y))
parser.ReportError("Expected Scale Z value, but got \"" + token + "\""); {
return false; // Not numeric!
} parser.ReportError("Expected Offset Y value, but got \"" + token + "\"");
break; return false;
}
#endregion
parser.SkipWhitespace(true);
#region ================== Offset token = parser.ReadToken();
if(!parser.ReadSignedFloat(token, ref offset.Z))
case "offset": {
parser.SkipWhitespace(true); // Not numeric!
token = parser.StripTokenQuotes(parser.ReadToken()); parser.ReportError("Expected Offset Z value, but got \"" + token + "\"");
if(!parser.ReadSignedFloat(token, ref offset.X)) return false;
{ }
// Not numeric! break;
parser.ReportError("Expected Offset X value, but got \"" + token + "\"");
return false; case "zoffset":
} parser.SkipWhitespace(true);
token = parser.ReadToken();
parser.SkipWhitespace(true); if(!parser.ReadSignedFloat(token, ref offset.Z))
token = parser.StripTokenQuotes(parser.ReadToken()); {
if(!parser.ReadSignedFloat(token, ref offset.Y)) // Not numeric!
{ parser.ReportError("Expected ZOffset value, but got \"" + token + "\"");
// Not numeric! return false;
parser.ReportError("Expected Offset Y value, but got \"" + token + "\""); }
return false; break;
}
case "angleoffset":
parser.SkipWhitespace(true); parser.SkipWhitespace(true);
token = parser.StripTokenQuotes(parser.ReadToken()); token = parser.ReadToken();
if(!parser.ReadSignedFloat(token, ref offset.Z)) if(!parser.ReadSignedFloat(token, ref angleoffset))
{ {
// Not numeric! // Not numeric!
parser.ReportError("Expected Offset Z value, but got \"" + token + "\""); parser.ReportError("Expected AngleOffset value, but got \"" + token + "\"");
return false; return false;
} }
break; break;
#endregion case "pitchoffset":
parser.SkipWhitespace(true);
#region ================== ZOffset token = parser.ReadToken();
if(!parser.ReadSignedFloat(token, ref pitchoffset))
case "zoffset": {
parser.SkipWhitespace(true); // Not numeric!
token = parser.StripTokenQuotes(parser.ReadToken()); parser.ReportError("Expected PitchOffset value, but got \"" + token + "\"");
if(!parser.ReadSignedFloat(token, ref offset.Z)) return false;
{ }
// Not numeric! break;
parser.ReportError("Expected ZOffset value, but got \"" + token + "\"");
return false; case "rolloffset":
} parser.SkipWhitespace(true);
break; token = parser.ReadToken();
if(!parser.ReadSignedFloat(token, ref rolloffset))
#endregion {
// Not numeric!
#region ================== AngleOffset parser.ReportError("Expected RollOffset value, but got \"" + token + "\"");
return false;
case "angleoffset": }
parser.SkipWhitespace(true); break;
token = parser.StripTokenQuotes(parser.ReadToken());
if(!parser.ReadSignedFloat(token, ref angleOffset)) case "inheritactorpitch": inheritactorpitch = true; break;
{ case "inheritactorroll": inheritactorroll = true; break;
// Not numeric!
parser.ReportError("Expected AngleOffset value, but got \"" + token + "\""); //FrameIndex <XXXX> <X> <model index> <frame number>
return false; case "frameindex":
} // Sprite name
break; parser.SkipWhitespace(true);
string fispritename = parser.ReadToken();
#endregion if(string.IsNullOrEmpty(fispritename))
{
#region ================== PitchOffset parser.ReportError("Expected sprite name");
return false;
case "pitchoffset": }
parser.SkipWhitespace(true); if(fispritename.Length != 4)
token = parser.StripTokenQuotes(parser.ReadToken()); {
if(!parser.ReadSignedFloat(token, ref pitchOffset)) parser.ReportError("Sprite name must be 4 characters long");
{ return false;
// Not numeric! }
parser.ReportError("Expected PitchOffset value, but got \"" + token + "\"");
return false; // Sprite frame
} parser.SkipWhitespace(true);
break; token = parser.ReadToken();
if(string.IsNullOrEmpty(token))
#endregion {
parser.ReportError("Expected sprite frame");
#region ================== RollOffset return false;
}
case "rolloffset": if(token.Length != 1)
parser.SkipWhitespace(true); {
token = parser.StripTokenQuotes(parser.ReadToken()); parser.ReportError("Sprite frame must be 1 character long");
if(!parser.ReadSignedFloat(token, ref rollOffset)) return false;
{ }
// Not numeric!
parser.ReportError("Expected RollOffset value, but got \"" + token + "\""); // Make full name
return false; fispritename += token;
}
break; // Model index
parser.SkipWhitespace(true);
#endregion int fimodelindnex;
token = parser.ReadToken();
#region ================== InheritActorPitch if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out fimodelindnex) || fimodelindnex < 0)
{
case "inheritactorpitch": // Not numeric!
inheritactorpitch = true; parser.ReportError("Expected model index, but got \"" + token + "\"");
break; return false;
}
#endregion
// Frame number
#region ================== InheritActorRoll parser.SkipWhitespace(true);
int fiframeindnex;
case "inheritactorroll": token = parser.ReadToken();
inheritactorroll = true; if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out fiframeindnex) || fiframeindnex < 0)
break; {
// Not numeric!
#endregion parser.ReportError("Expected frame index, but got \"" + token + "\"");
return false;
#region ================== Frame / FrameIndex }
case "frameindex": // Add to collection
case "frame": FrameStructure fifs = new FrameStructure { FrameIndex = fiframeindnex, ModelIndex = fimodelindnex, SpriteName = fispritename };
//parsed all required fields. if got more than one model - find which one(s) should be displayed if(!frames.ContainsKey(fispritename))
if(modelNames.GetLength(0) > 1) {
{ frames.Add(fispritename, new HashSet<FrameStructure>());
string spriteLump = null; frames[fispritename].Add(fifs);
string spriteFrame = null; }
else if(frames[fispritename].Contains(fifs))
//step back {
parser.DataStream.Seek(-token.Length - 1, SeekOrigin.Current); parser.LogWarning("Duplicate FrameIndex definition");
}
//here we check which models are used in first encountered lump and frame else
while(parser.SkipWhitespace(true)) {
{ frames[fispritename].Add(fifs);
token = parser.StripTokenQuotes(parser.ReadToken()).ToLowerInvariant(); }
if(token == "frameindex" || token == "frame") break;
{
bool frameIndex = (token == "frameindex"); //Frame <XXXX> <X> <model index> <"frame name">
parser.SkipWhitespace(true); case "frame":
// Sprite name
//should be sprite lump parser.SkipWhitespace(true);
token = parser.StripTokenQuotes(parser.ReadToken()).ToLowerInvariant(); string spritename = parser.ReadToken();
if(string.IsNullOrEmpty(spriteLump)) if(string.IsNullOrEmpty(spritename))
{ {
spriteLump = token; parser.ReportError("Expected sprite name");
} return false;
else if(spriteLump != token) //got another lump }
{ if(spritename.Length != 4)
for(int i = 0; i < modelsUsed.Length; i++) {
{ parser.ReportError("Sprite name must be 4 characters long");
if(!modelsUsed[i]) return false;
{ }
modelNames[i] = null;
textureNames[i] = null; // Sprite frame
} parser.SkipWhitespace(true);
} token = parser.ReadToken();
break; if(string.IsNullOrEmpty(token))
} {
parser.ReportError("Expected sprite frame");
parser.SkipWhitespace(true); return false;
}
//should be sprite frame if(token.Length != 1)
token = parser.StripTokenQuotes(parser.ReadToken()).ToLowerInvariant(); {
if(string.IsNullOrEmpty(spriteFrame)) parser.ReportError("Sprite frame must be 1 character long");
{ return false;
spriteFrame = token; }
}
else if(spriteFrame != token) //got another frame // Make full name
{ spritename += token;
for(int i = 0; i < modelsUsed.Length; i++)
{ // Model index
if(!modelsUsed[i]) parser.SkipWhitespace(true);
{ int modelindnex;
modelNames[i] = null; token = parser.ReadToken();
textureNames[i] = null; if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out modelindnex) || modelindnex < 0)
} {
} // Not numeric!
break; parser.ReportError("Expected model index, but got \"" + token + "\"");
} return false;
}
parser.SkipWhitespace(true);
// Frame name
//should be model index parser.SkipWhitespace(true);
token = parser.StripTokenQuotes(parser.ReadToken()); string framename = parser.StripTokenQuotes(parser.ReadToken());
if(string.IsNullOrEmpty(framename))
int modelIndex; {
if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out modelIndex)) parser.ReportError("Expected frame name");
{ return false;
// Not numeric! }
parser.ReportError("Expected model index, but got \"" + token + "\"");
return false; // Add to collection
} FrameStructure fs = new FrameStructure { FrameName = framename, ModelIndex = modelindnex, SpriteName = spritename };
if(!frames.ContainsKey(spritename))
if(modelIndex >= MAX_MODELS) {
{ frames.Add(spritename, new HashSet<FrameStructure>());
parser.ReportError("GZDoom doesn't allow more than " + MAX_MODELS + " models per MODELDEF entry"); frames[spritename].Add(fs);
return false; }
} else if(frames[spritename].Contains(fs))
{
if(modelNames[modelIndex] == null) parser.LogWarning("Duplicate Frame definition");
{ }
parser.ReportError("Model index doesn't correspond to any defined model"); else
return false; {
} frames[spritename].Add(fs);
}
modelsUsed[modelIndex] = true; break;
parser.SkipWhitespace(true); case "{":
parser.ReportError("Unexpected scope start");
// Should be frame name or index return false;
token = parser.StripTokenQuotes(parser.ReadToken());
if(frameIndex) // Structure ends here
{ case "}":
int frame = 0; parsingfinished = true;
if(!parser.ReadSignedInt(token, ref frame)) break;
{
// Not numeric!
parser.ReportError("Expected model frame index, but got \"" + token + "\"");
return false;
}
// Skip the model if frame index is -1
if(frame == -1) modelsUsed[modelIndex] = false;
else frameIndices[modelIndex] = frame;
}
else
{
if(string.IsNullOrEmpty(token))
{
// Missing!
parser.ReportError("Expected model frame name");
return false;
}
frameNames[modelIndex] = token.ToLowerInvariant();
}
}
else
{
//must be "}", step back
parser.DataStream.Seek(-token.Length - 1, SeekOrigin.Current);
break;
}
}
}
parsingfinished = true;
break;
#endregion
}
} }
} }
// Find closing brace, then quit // Perform some integrity checks
while(parser.SkipWhitespace(true)) if(!parsingfinished)
{ {
token = parser.ReadToken(); parser.ReportError("Unclosed structure scope");
if(string.IsNullOrEmpty(token) || token == "}") break;
}
// Bail out when got errors or no models are used
if(Array.IndexOf(modelsUsed, true) == -1)
{
parser.ReportError("No models are used by \"" + classname + "\"");
return false; return false;
} }
// Classname is set in ModeldefParser // Any models defined?
ModelData = new ModelData { InheritActorPitch = inheritactorpitch, InheritActorRoll = inheritactorroll }; bool valid = false;
Matrix moffset = Matrix.Translation(offset.Y, -offset.X, offset.Z); // Things are complicated in GZDoom... for(int i = 0; i < modelnames.Length; i++)
Matrix mrotation = Matrix.RotationY(-Angle2D.DegToRad(rollOffset)) * Matrix.RotationX(-Angle2D.DegToRad(pitchOffset)) * Matrix.RotationZ(Angle2D.DegToRad(angleOffset));
ModelData.SetTransform(mrotation, moffset, scale);
for(int i = 0; i < modelNames.Length; i++)
{ {
if(!string.IsNullOrEmpty(modelNames[i]) && modelsUsed[i]) if(!string.IsNullOrEmpty(modelnames[i]))
{ {
ModelData.TextureNames.Add(string.IsNullOrEmpty(textureNames[i]) ? string.Empty : textureNames[i].ToLowerInvariant()); //INFO: skin may be defined in the model itself, so we don't check it here
ModelData.ModelNames.Add(modelNames[i].ToLowerInvariant()); valid = true;
ModelData.FrameNames.Add(frameNames[i]); break;
ModelData.FrameIndices.Add(frameIndices[i]);
} }
} }
if(ModelData.ModelNames.Count == 0) if(!valid)
{ {
parser.ReportError("\"" + classname + "\" has no models"); parser.ReportError("Structure doesn't define any models");
return false; return false;
} }
// Check skin-model associations
for(int i = 0; i < texturenames.Length; i++)
{
if(!string.IsNullOrEmpty(texturenames[i]) && string.IsNullOrEmpty(modelnames[i]))
{
parser.ReportError("No model is defined for skin " + i + ":\"" + texturenames[i] + "\"");
return false;
}
}
return true; return true;
} }
#endregion
} }
} }

View file

@ -375,11 +375,11 @@ namespace CodeImp.DoomBuilder
General.Map.Graphics.Reset(); General.Map.Graphics.Reset();
General.MainWindow.RedrawDisplay(); General.MainWindow.RedrawDisplay();
} }
else if(General.Editing.Mode is VisualMode) /*else if(General.Editing.Mode is VisualMode)
{ {
//General.MainWindow.StopExclusiveMouseInput(); General.MainWindow.StopExclusiveMouseInput();
//General.MainWindow.StartExclusiveMouseInput(); General.MainWindow.StartExclusiveMouseInput();
} }*/
} }
General.MainWindow.FocusDisplay(); General.MainWindow.FocusDisplay();

View file

@ -1615,7 +1615,7 @@ namespace CodeImp.DoomBuilder.Rendering
graphics.Device.SetRenderState(RenderState.FogEnable, false); graphics.Device.SetRenderState(RenderState.FogEnable, false);
graphics.Shaders.Display2D.Texture1 = label.Texture; graphics.Shaders.Display2D.Texture1 = label.Texture;
SetWorldTransformation(false); SetWorldTransformation(false);
graphics.Shaders.Display2D.SetSettings(1f, 1f, 0f, 1f, true); graphics.Shaders.Display2D.SetSettings(1f, 1f, 0f, 1f, false);
graphics.Device.SetStreamSource(0, label.VertexBuffer, 0, FlatVertex.Stride); graphics.Device.SetStreamSource(0, label.VertexBuffer, 0, FlatVertex.Stride);
// Draw // Draw
@ -1648,7 +1648,7 @@ namespace CodeImp.DoomBuilder.Rendering
graphics.Device.SetRenderState(RenderState.TextureFactor, -1); graphics.Device.SetRenderState(RenderState.TextureFactor, -1);
graphics.Device.SetRenderState(RenderState.FogEnable, false); graphics.Device.SetRenderState(RenderState.FogEnable, false);
SetWorldTransformation(false); SetWorldTransformation(false);
graphics.Shaders.Display2D.SetSettings(1f, 1f, 0f, 1f, true); graphics.Shaders.Display2D.SetSettings(1f, 1f, 0f, 1f, false);
// Begin drawing // Begin drawing
graphics.Shaders.Display2D.Begin(); graphics.Shaders.Display2D.Begin();

View file

@ -88,7 +88,7 @@ namespace CodeImp.DoomBuilder.Rendering
public TextAlignmentX AlignX { get { return alignx; } set { alignx = value; updateneeded = true; } } public TextAlignmentX AlignX { get { return alignx; } set { alignx = value; updateneeded = true; } }
public TextAlignmentY AlignY { get { return aligny; } set { aligny = value; updateneeded = true; } } public TextAlignmentY AlignY { get { return aligny; } set { aligny = value; updateneeded = true; } }
public PixelColor Color { get { return color; } set { if(!color.Equals(value)) { color = value; textureupdateneeded = true; } } } public PixelColor Color { get { return color; } set { if(!color.Equals(value)) { color = value; textureupdateneeded = true; } } }
public PixelColor Backcolor { get { return backcolor; } set { if(!backcolor.Equals(value)) { backcolor = value; textureupdateneeded = true; } } } public PixelColor BackColor { get { return backcolor; } set { if(!backcolor.Equals(value)) { backcolor = value; textureupdateneeded = true; } } }
public bool DrawBackground { get { return drawbg; } set { if(drawbg != value) { drawbg = value; textureupdateneeded = true; } } } //mxd public bool DrawBackground { get { return drawbg; } set { if(drawbg != value) { drawbg = value; textureupdateneeded = true; } } } //mxd
internal Texture Texture { get { return texture; } } //mxd internal Texture Texture { get { return texture; } } //mxd
internal VertexBuffer VertexBuffer { get { return textbuffer; } } internal VertexBuffer VertexBuffer { get { return textbuffer; } }
@ -109,7 +109,7 @@ namespace CodeImp.DoomBuilder.Rendering
this.font = General.Settings.TextLabelFont; //mxd this.font = General.Settings.TextLabelFont; //mxd
this.rect = new RectangleF(0f, 0f, 1f, 1f); this.rect = new RectangleF(0f, 0f, 1f, 1f);
this.color = new PixelColor(255, 255, 255, 255); this.color = new PixelColor(255, 255, 255, 255);
this.backcolor = new PixelColor(255, 0, 0, 0); this.backcolor = new PixelColor(128, 0, 0, 0);
this.alignx = TextAlignmentX.Center; this.alignx = TextAlignmentX.Center;
this.aligny = TextAlignmentY.Top; this.aligny = TextAlignmentY.Top;
this.textsize = new SizeF(); this.textsize = new SizeF();
@ -119,10 +119,6 @@ namespace CodeImp.DoomBuilder.Rendering
// Register as resource // Register as resource
General.Map.Graphics.RegisterResource(this); General.Map.Graphics.RegisterResource(this);
//mxd. Create the buffer
this.textbuffer = new VertexBuffer(General.Map.Graphics.Device, 4 * FlatVertex.Stride,
Usage.Dynamic | Usage.WriteOnly, VertexFormat.None, Pool.Default);
// We have no destructor // We have no destructor
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
@ -203,7 +199,7 @@ namespace CodeImp.DoomBuilder.Rendering
} }
// Create label image // Create label image
Bitmap img = CreateLabelImage(text, font, color, backcolor, drawbg); Bitmap img = CreateLabelImage(text, font, alignx, aligny, color, backcolor, drawbg);
textsize = img.Size; textsize = img.Size;
// Create texture // Create texture
@ -234,6 +230,13 @@ namespace CodeImp.DoomBuilder.Rendering
case TextAlignmentY.Bottom: beginy = absview.Y + absview.Height - textsize.Height; break; case TextAlignmentY.Bottom: beginy = absview.Y + absview.Height - textsize.Height; break;
} }
//mxd. Create the buffer
if(textbuffer == null || textbuffer.Disposed)
{
textbuffer = new VertexBuffer(General.Map.Graphics.Device, 4 * FlatVertex.Stride,
Usage.Dynamic | Usage.WriteOnly, VertexFormat.None, Pool.Default);
}
//mxd. Lock the buffer //mxd. Lock the buffer
using(DataStream stream = textbuffer.Lock(0, 4 * FlatVertex.Stride, LockFlags.Discard | LockFlags.NoSystemLock)) using(DataStream stream = textbuffer.Lock(0, 4 * FlatVertex.Stride, LockFlags.Discard | LockFlags.NoSystemLock))
{ {
@ -258,7 +261,7 @@ namespace CodeImp.DoomBuilder.Rendering
} }
//mxd //mxd
private static Bitmap CreateLabelImage(string text, Font font, PixelColor color, PixelColor backcolor, bool drawbg) private static Bitmap CreateLabelImage(string text, Font font, TextAlignmentX alignx, TextAlignmentY aligny, PixelColor color, PixelColor backcolor, bool drawbg)
{ {
PointF textorigin = new PointF(4, 3); PointF textorigin = new PointF(4, 3);
RectangleF textrect = new RectangleF(textorigin, General.Interface.MeasureString(text, font)); RectangleF textrect = new RectangleF(textorigin, General.Interface.MeasureString(text, font));
@ -266,7 +269,25 @@ namespace CodeImp.DoomBuilder.Rendering
textrect.Height = (float)Math.Round(textrect.Height); textrect.Height = (float)Math.Round(textrect.Height);
RectangleF bgrect = new RectangleF(0, 0, textrect.Width + textorigin.X * 2, textrect.Height + textorigin.Y * 2); RectangleF bgrect = new RectangleF(0, 0, textrect.Width + textorigin.X * 2, textrect.Height + textorigin.Y * 2);
Bitmap result = new Bitmap((int)bgrect.Width, (int)bgrect.Height); // Make PO2 image, for speed and giggles...
RectangleF po2rect = new RectangleF(0, 0, General.NextPowerOf2((int)bgrect.Width), General.NextPowerOf2((int)bgrect.Height));
switch(alignx)
{
case TextAlignmentX.Center: bgrect.X = (po2rect.Width - bgrect.Width) / 2; break;
case TextAlignmentX.Right: bgrect.X = po2rect.Width - bgrect.Width; break;
}
switch(aligny)
{
case TextAlignmentY.Middle: bgrect.Y = (po2rect.Height - bgrect.Height) / 2; break;
case TextAlignmentY.Bottom: bgrect.Y = po2rect.Height - bgrect.Height; break;
}
textrect.X += bgrect.X;
textrect.Y += bgrect.Y;
Bitmap result = new Bitmap((int)po2rect.Width, (int)po2rect.Height);
using(Graphics g = Graphics.FromImage(result)) using(Graphics g = Graphics.FromImage(result))
{ {
g.SmoothingMode = SmoothingMode.HighQuality; g.SmoothingMode = SmoothingMode.HighQuality;
@ -276,7 +297,7 @@ namespace CodeImp.DoomBuilder.Rendering
// Draw text // Draw text
using(StringFormat sf = new StringFormat()) using(StringFormat sf = new StringFormat())
{ {
sf.FormatFlags = StringFormatFlags.NoWrap; sf.FormatFlags = StringFormatFlags.FitBlackBox | StringFormatFlags.NoWrap;
sf.Alignment = StringAlignment.Center; sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center;
@ -319,21 +340,20 @@ namespace CodeImp.DoomBuilder.Rendering
using(SolidBrush brush = new SolidBrush(backcolor.ToColor())) using(SolidBrush brush = new SolidBrush(backcolor.ToColor()))
g.DrawString(text, font, brush, textrect, sf); g.DrawString(text, font, brush, textrect, sf);
} }
// Draw text with outline // Draw plain text
else else
{ {
RectangleF pathrect = textrect; RectangleF plainbgrect = textrect;
pathrect.Inflate(1, 3); if(text.Length > 1) plainbgrect.Inflate(6, 2);
GraphicsPath p = new GraphicsPath(); RectangleF plaintextrect = textrect;
p.AddString(text, font.FontFamily, (int)font.Style, g.DpiY * font.Size / 72f, pathrect, sf); plaintextrect.Inflate(6, 4);
// Draw'n'fill text using(SolidBrush brush = new SolidBrush(backcolor.ToColor()))
using(Pen pen = new Pen(backcolor.ToColor(), 3)) g.FillRectangle(brush, plainbgrect);
g.DrawPath(pen, p);
using(SolidBrush brush = new SolidBrush(color.ToColor())) using(SolidBrush brush = new SolidBrush(color.ToColor()))
g.FillPath(brush, p); g.DrawString(text, font, brush, plaintextrect, sf);
} }
} }
} }

View file

@ -204,16 +204,19 @@ namespace CodeImp.DoomBuilder.Windows
// Constructor // Constructor
internal MainForm() internal MainForm()
{ {
//mxd. Set DPI-aware icon size // Fetch pointer
using(Graphics g = this.CreateGraphics()) windowptr = base.Handle;
{
DPIScaler = new SizeF(g.DpiX / 96, g.DpiY / 96);
if(DPIScaler.Width != 1.0f || DPIScaler.Height != 1.0f) //mxd. Graphics
{ graphics = Graphics.FromHwndInternal(windowptr);
ScaledIconSize.Width = (int)Math.Round(ScaledIconSize.Width * DPIScaler.Width);
ScaledIconSize.Height = (int)Math.Round(ScaledIconSize.Height * DPIScaler.Height); //mxd. Set DPI-aware icon size
} DPIScaler = new SizeF(graphics.DpiX / 96, graphics.DpiY / 96);
if(DPIScaler.Width != 1.0f || DPIScaler.Height != 1.0f)
{
ScaledIconSize.Width = (int)Math.Round(ScaledIconSize.Width * DPIScaler.Width);
ScaledIconSize.Height = (int)Math.Round(ScaledIconSize.Height * DPIScaler.Height);
} }
// Setup controls // Setup controls
@ -234,9 +237,6 @@ namespace CodeImp.DoomBuilder.Windows
labelcollapsedinfo.Text = ""; labelcollapsedinfo.Text = "";
display.Dock = DockStyle.Fill; display.Dock = DockStyle.Fill;
// Fetch pointer
windowptr = base.Handle;
// Make array for view modes // Make array for view modes
viewmodesbuttons = new ToolStripButton[Renderer2D.NUM_VIEW_MODES]; viewmodesbuttons = new ToolStripButton[Renderer2D.NUM_VIEW_MODES];
viewmodesbuttons[(int)ViewMode.Normal] = buttonviewnormal; viewmodesbuttons[(int)ViewMode.Normal] = buttonviewnormal;
@ -290,9 +290,6 @@ namespace CodeImp.DoomBuilder.Windows
//mxd. Hints //mxd. Hints
hintsPanel = new HintsPanel(); hintsPanel = new HintsPanel();
hintsDocker = new Docker("hints", "Help", hintsPanel); hintsDocker = new Docker("hints", "Help", hintsPanel);
//mxd. Graphics
graphics = Graphics.FromHwndInternal(windowptr);
} }
#endregion #endregion
@ -2679,14 +2676,14 @@ namespace CodeImp.DoomBuilder.Windows
private string GetDisplayFilename(string filename) private string GetDisplayFilename(string filename)
{ {
// String doesnt fit? // String doesnt fit?
if(GetStringWidth(filename) > MAX_RECENT_FILES_PIXELS) if(MeasureString(filename, this.Font).Width > MAX_RECENT_FILES_PIXELS)
{ {
// Start chopping off characters // Start chopping off characters
for(int i = filename.Length - 6; i >= 0; i--) for(int i = filename.Length - 6; i >= 0; i--)
{ {
// Does it fit now? // Does it fit now?
string newname = filename.Substring(0, 3) + "..." + filename.Substring(filename.Length - i, i); string newname = filename.Substring(0, 3) + "..." + filename.Substring(filename.Length - i, i);
if(GetStringWidth(newname) <= MAX_RECENT_FILES_PIXELS) return newname; if(MeasureString(newname, this.Font).Width <= MAX_RECENT_FILES_PIXELS) return newname;
} }
// Cant find anything that fits (most unlikely!) // Cant find anything that fits (most unlikely!)
@ -2699,14 +2696,6 @@ namespace CodeImp.DoomBuilder.Windows
} }
} }
// This returns the width of a string
private float GetStringWidth(string str)
{
Graphics g = Graphics.FromHwndInternal(this.Handle);
SizeF strsize = g.MeasureString(str, this.Font);
return strsize.Width;
}
// Exit clicked // Exit clicked
private void itemexit_Click(object sender, EventArgs e) { this.Close(); } private void itemexit_Click(object sender, EventArgs e) { this.Close(); }

View file

@ -395,7 +395,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
l.AlignX = TextAlignmentX.Center; l.AlignX = TextAlignmentX.Center;
l.AlignY = TextAlignmentY.Middle; l.AlignY = TextAlignmentY.Middle;
l.Color = General.Colors.InfoLine; l.Color = General.Colors.InfoLine;
l.Backcolor = General.Colors.Background.WithAlpha(255); l.BackColor = General.Colors.Background.WithAlpha(128);
larr[i] = l; larr[i] = l;
} }
@ -430,7 +430,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
l.AlignX = TextAlignmentX.Center; l.AlignX = TextAlignmentX.Center;
l.AlignY = TextAlignmentY.Middle; l.AlignY = TextAlignmentY.Middle;
l.Color = (linedef == highlighted ? General.Colors.Selection : General.Colors.Highlight); l.Color = (linedef == highlighted ? General.Colors.Selection : General.Colors.Highlight);
l.Backcolor = General.Colors.Background.WithAlpha(255); l.BackColor = General.Colors.Background.WithAlpha(192);
l.Text = (++index).ToString(); l.Text = (++index).ToString();
labels.Add(linedef, l); labels.Add(linedef, l);
} }

View file

@ -160,7 +160,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
labelarray[i].AlignX = TextAlignmentX.Center; labelarray[i].AlignX = TextAlignmentX.Center;
labelarray[i].AlignY = TextAlignmentY.Middle; labelarray[i].AlignY = TextAlignmentY.Middle;
labelarray[i].Color = c; labelarray[i].Color = c;
labelarray[i].Backcolor = General.Colors.Background.WithAlpha(255); labelarray[i].BackColor = General.Colors.Background.WithAlpha(128);
} }
labels.Add(s, labelarray); labels.Add(s, labelarray);
} }

View file

@ -936,7 +936,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
l.AlignX = TextAlignmentX.Center; l.AlignX = TextAlignmentX.Center;
l.AlignY = TextAlignmentY.Middle; l.AlignY = TextAlignmentY.Middle;
l.Color = General.Colors.InfoLine; l.Color = General.Colors.InfoLine;
l.Backcolor = General.Colors.Background.WithAlpha(255); l.BackColor = General.Colors.Background.WithAlpha(128);
larr[i] = l; larr[i] = l;
} }
@ -982,8 +982,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
} }
l.Color = (thing == highlighted ? General.Colors.Selection : General.Colors.Highlight); l.Color = (thing == highlighted ? General.Colors.Selection : General.Colors.Highlight);
l.Backcolor = General.Colors.Background.WithAlpha(255); l.BackColor = General.Colors.Background.WithAlpha(192);
l.DrawBackground = true;
l.Text = (++index).ToString(); l.Text = (++index).ToString();
labels.Add(thing, l); labels.Add(thing, l);
} }

View file

@ -103,12 +103,14 @@ namespace CodeImp.DoomBuilder.BuilderModes
// Initialization // Initialization
private void Initialize() private void Initialize()
{ {
label = new TextLabel(); label = new TextLabel
label.AlignX = TextAlignmentX.Center; {
label.AlignY = TextAlignmentY.Middle; AlignX = TextAlignmentX.Center,
label.Color = General.Colors.Highlight; AlignY = TextAlignmentY.Middle,
label.Backcolor = General.Colors.Background; Color = General.Colors.Highlight,
label.TransformCoords = true; BackColor = General.Colors.Background.WithAlpha(64),
TransformCoords = true,
};
} }
// Disposer // Disposer