mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-12-25 17:59:24 +01:00
part 2: format state and making the codes do the work
This commit is contained in:
parent
b480026b7e
commit
02b25236b1
5 changed files with 137 additions and 85 deletions
|
@ -96,11 +96,11 @@ namespace Demos {
|
||||||
|
|
||||||
// adding some custom image formatting codes
|
// adding some custom image formatting codes
|
||||||
// note that all added formatting codes need to be lowercase, while their casing doesn't matter when used
|
// note that all added formatting codes need to be lowercase, while their casing doesn't matter when used
|
||||||
TextFormatting.FormattingCodes[new Regex("grass")] = m => new FormattingCode(image.Texture);
|
TextFormatting.FormattingCodes[new Regex("grass")] = (m, i) => new FormattingCode(i, image.Texture);
|
||||||
TextFormatting.FormattingCodes[new Regex("tree")] = m => new FormattingCode(tree);
|
TextFormatting.FormattingCodes[new Regex("tree")] = (m, i) => new FormattingCode(i, tree);
|
||||||
// formatting codes can also be sprite animations!
|
// formatting codes can also be sprite animations!
|
||||||
var atlas = new UniformTextureAtlas(LoadContent<Texture2D>("Textures/Anim"), 4, 4);
|
var atlas = new UniformTextureAtlas(LoadContent<Texture2D>("Textures/Anim"), 4, 4);
|
||||||
TextFormatting.FormattingCodes[new Regex("walk")] = m => new FormattingCode(new SpriteAnimation(0.2F, atlas[0, 0], atlas[0, 1], atlas[0, 2], atlas[0, 3]));
|
TextFormatting.FormattingCodes[new Regex("walk")] = (m, i) => new FormattingCode(i, new SpriteAnimation(0.2F, atlas[0, 0], atlas[0, 1], atlas[0, 2], atlas[0, 3]));
|
||||||
|
|
||||||
root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Additionally, you can create custom formatting codes that contain [Grass] images or [Walk] sprite animations! Note that these images have to be square, or [Tree] bad things happen."));
|
root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Additionally, you can create custom formatting codes that contain [Grass] images or [Walk] sprite animations! Note that these images have to be square, or [Tree] bad things happen."));
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Font;
|
|
||||||
|
|
||||||
namespace MLEM.Formatting {
|
namespace MLEM.Formatting {
|
||||||
public class AnimationCode : FormattingCode {
|
public class AnimationCode : FormattingCode {
|
||||||
|
|
||||||
public static readonly DrawCharacter Default = (code, settings, font, batch, totalText, index, effectStartIndex, charSt, position, color, scale, layerDepth) => {
|
public readonly FormatState.DrawCharacter Draw;
|
||||||
font.DrawString(batch, charSt, position, color, 0, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
|
|
||||||
};
|
|
||||||
public static readonly DrawCharacter Wobbly = (code, settings, font, batch, totalText, index, effectStartIndex, charSt, position, color, scale, layerDepth) => {
|
|
||||||
var offset = new Vector2(0, (float) Math.Sin(index + code.Time.TotalSeconds * settings.WobbleModifier) * font.LineHeight * settings.WobbleHeightModifier * scale);
|
|
||||||
font.DrawString(batch, charSt, position + offset, color, 0, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
|
|
||||||
};
|
|
||||||
public static readonly DrawCharacter Typing = (code, settings, font, batch, totalText, index, effectStartIndex, charSt, position, color, scale, layerDepth) => {
|
|
||||||
if (code.Time.TotalSeconds * settings.TypingSpeed > index - effectStartIndex + 1)
|
|
||||||
font.DrawString(batch, charSt, position, color, 0, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
|
|
||||||
};
|
|
||||||
public static readonly AnimationCode DefaultCode = new AnimationCode(Default);
|
|
||||||
|
|
||||||
public readonly DrawCharacter Draw;
|
|
||||||
public TimeSpan Time;
|
public TimeSpan Time;
|
||||||
|
|
||||||
public AnimationCode(DrawCharacter draw) : base(Type.Animation) {
|
public AnimationCode(int startIndex, FormatState.DrawCharacter draw) : base(startIndex, Type.Animation) {
|
||||||
this.Draw = draw;
|
this.Draw = draw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +20,9 @@ namespace MLEM.Formatting {
|
||||||
this.Time = TimeSpan.Zero;
|
this.Time = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate void DrawCharacter(AnimationCode code, FormatSettings settings, IGenericFont font, SpriteBatch batch, string totalText, int index, int effectStartIndex, string charSt, Vector2 position, Color color, float scale, float layerDepth);
|
public override void ModifyFormatting(ref FormatState state) {
|
||||||
|
state.DrawBehavior = this.Draw;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
30
MLEM/Formatting/FormatState.cs
Normal file
30
MLEM/Formatting/FormatState.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MLEM.Font;
|
||||||
|
|
||||||
|
namespace MLEM.Formatting {
|
||||||
|
public struct FormatState {
|
||||||
|
|
||||||
|
public static readonly DrawCharacter DefaultDrawBehavior = (state, batch, totalText, index, charSt, position, scale, layerDepth) => {
|
||||||
|
state.Font.DrawString(batch, charSt, position, state.Color, 0, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
|
||||||
|
};
|
||||||
|
public FormattingCodeCollection FormattingCodes { get; internal set; }
|
||||||
|
public IGenericFont RegularFont { get; internal set; }
|
||||||
|
public IGenericFont ItalicFont { get; internal set; }
|
||||||
|
public IGenericFont BoldFont { get; internal set; }
|
||||||
|
public FormatSettings Settings { get; internal set; }
|
||||||
|
public int CurrentIndex { get; internal set; }
|
||||||
|
public Vector2 InnerOffset { get; internal set; }
|
||||||
|
|
||||||
|
public Color Color;
|
||||||
|
public IGenericFont Font;
|
||||||
|
public DrawCharacter DrawBehavior;
|
||||||
|
|
||||||
|
public void PrependBehavior(DrawCharacter behavior) {
|
||||||
|
this.DrawBehavior = behavior + this.DrawBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void DrawCharacter(FormatState state, SpriteBatch batch, string totalText, int index, string charSt, Vector2 position, float scale, float layerDepth);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
|
using System;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Animations;
|
using MLEM.Animations;
|
||||||
using MLEM.Font;
|
using MLEM.Font;
|
||||||
|
using MLEM.Misc;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
|
|
||||||
namespace MLEM.Formatting {
|
namespace MLEM.Formatting {
|
||||||
|
@ -10,24 +13,26 @@ namespace MLEM.Formatting {
|
||||||
public readonly Color Color;
|
public readonly Color Color;
|
||||||
public readonly TextStyle Style;
|
public readonly TextStyle Style;
|
||||||
public readonly SpriteAnimation Icon;
|
public readonly SpriteAnimation Icon;
|
||||||
|
public readonly int StartIndex;
|
||||||
|
|
||||||
protected FormattingCode(Type type) {
|
protected FormattingCode(int startIndex, Type type) {
|
||||||
this.CodeType = type;
|
this.CodeType = type;
|
||||||
|
this.StartIndex = startIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FormattingCode(Color color) : this(Type.Color) {
|
public FormattingCode(int startIndex, Color color) : this(startIndex, Type.Color) {
|
||||||
this.Color = color;
|
this.Color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FormattingCode(TextStyle style) : this(Type.Style) {
|
public FormattingCode(int startIndex, TextStyle style) : this(startIndex, Type.Style) {
|
||||||
this.Style = style;
|
this.Style = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FormattingCode(TextureRegion icon) :
|
public FormattingCode(int startIndex, TextureRegion icon) :
|
||||||
this(new SpriteAnimation(0, icon)) {
|
this(startIndex, new SpriteAnimation(0, icon)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public FormattingCode(SpriteAnimation icon) : this(Type.Icon) {
|
public FormattingCode(int startIndex, SpriteAnimation icon) : this(startIndex, Type.Icon) {
|
||||||
this.Icon = icon;
|
this.Icon = icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +50,46 @@ namespace MLEM.Formatting {
|
||||||
this.Icon.Restart();
|
this.Icon.Restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void ModifyFormatting(ref FormatState state) {
|
||||||
|
switch (this.CodeType) {
|
||||||
|
case Type.Color:
|
||||||
|
state.Color = this.Color;
|
||||||
|
break;
|
||||||
|
case Type.Style:
|
||||||
|
switch (this.Style) {
|
||||||
|
case TextStyle.Regular:
|
||||||
|
state.DrawBehavior = FormatState.DefaultDrawBehavior;
|
||||||
|
state.Font = state.RegularFont;
|
||||||
|
break;
|
||||||
|
case TextStyle.Bold:
|
||||||
|
state.Font = state.BoldFont;
|
||||||
|
break;
|
||||||
|
case TextStyle.Italic:
|
||||||
|
state.Font = state.ItalicFont;
|
||||||
|
break;
|
||||||
|
case TextStyle.Shadow:
|
||||||
|
var formatState = state;
|
||||||
|
state.PrependBehavior((s, batch, totalText, index, charSt, position, scale, layerDepth) => {
|
||||||
|
var lastColor = s.Color;
|
||||||
|
s.Color = s.Settings.DropShadowColor;
|
||||||
|
// Call the last behavior to draw the shadow
|
||||||
|
formatState.DrawBehavior(s, batch, totalText, index, charSt, position + s.Settings.DropShadowOffset * scale, scale, layerDepth);
|
||||||
|
s.Color = lastColor;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Type.Icon:
|
||||||
|
state.PrependBehavior((s, batch, totalText, index, charSt, position, scale, layerDepth) => {
|
||||||
|
if (index == this.StartIndex)
|
||||||
|
batch.Draw(this.Icon.CurrentRegion, new RectangleF(position, new Vector2(s.RegularFont.LineHeight * scale)), s.Color, 0, Vector2.Zero, SpriteEffects.None, layerDepth);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
|
|
||||||
Color,
|
Color,
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MLEM.Animations;
|
||||||
using MLEM.Extensions;
|
using MLEM.Extensions;
|
||||||
using MLEM.Font;
|
using MLEM.Font;
|
||||||
using MLEM.Misc;
|
using MLEM.Misc;
|
||||||
|
@ -11,7 +12,7 @@ using MLEM.Textures;
|
||||||
namespace MLEM.Formatting {
|
namespace MLEM.Formatting {
|
||||||
public static class TextFormatting {
|
public static class TextFormatting {
|
||||||
|
|
||||||
public static readonly Dictionary<Regex, Func<Match, FormattingCode>> FormattingCodes = new Dictionary<Regex, Func<Match, FormattingCode>>();
|
public static readonly Dictionary<Regex, Func<Match, int, FormattingCode>> FormattingCodes = new Dictionary<Regex, Func<Match, int, FormattingCode>>();
|
||||||
private static readonly Dictionary<IGenericFont, string> OneEmStrings = new Dictionary<IGenericFont, string>();
|
private static readonly Dictionary<IGenericFont, string> OneEmStrings = new Dictionary<IGenericFont, string>();
|
||||||
private static Regex formatRegex;
|
private static Regex formatRegex;
|
||||||
|
|
||||||
|
@ -19,22 +20,30 @@ namespace MLEM.Formatting {
|
||||||
SetFormatIndicators('[', ']');
|
SetFormatIndicators('[', ']');
|
||||||
|
|
||||||
// style codes
|
// style codes
|
||||||
FormattingCodes[new Regex("regular")] = m => new FormattingCode(TextStyle.Regular);
|
FormattingCodes[new Regex("regular")] = (m, i) => new FormattingCode(i, TextStyle.Regular);
|
||||||
FormattingCodes[new Regex("italic")] = m => new FormattingCode(TextStyle.Italic);
|
FormattingCodes[new Regex("italic")] = (m, i) => new FormattingCode(i, TextStyle.Italic);
|
||||||
FormattingCodes[new Regex("bold")] = m => new FormattingCode(TextStyle.Bold);
|
FormattingCodes[new Regex("bold")] = (m, i) => new FormattingCode(i, TextStyle.Bold);
|
||||||
FormattingCodes[new Regex("shadow")] = m => new FormattingCode(TextStyle.Shadow);
|
FormattingCodes[new Regex("shadow")] = (m, i) => new FormattingCode(i, TextStyle.Shadow);
|
||||||
|
|
||||||
// color codes
|
// color codes
|
||||||
var colors = typeof(Color).GetProperties();
|
var colors = typeof(Color).GetProperties();
|
||||||
foreach (var color in colors) {
|
foreach (var color in colors) {
|
||||||
if (color.GetGetMethod().IsStatic)
|
if (color.GetGetMethod().IsStatic)
|
||||||
FormattingCodes[new Regex(color.Name.ToLowerInvariant())] = m => new FormattingCode((Color) color.GetValue(null));
|
FormattingCodes[new Regex(color.Name.ToLowerInvariant())] = (m, i) => new FormattingCode(i, (Color) color.GetValue(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
// animations
|
// animations
|
||||||
FormattingCodes[new Regex("unanimated")] = m => new AnimationCode(AnimationCode.Default);
|
FormattingCodes[new Regex("unanimated")] = (m, i) => new AnimationCode(i, FormatState.DefaultDrawBehavior);
|
||||||
FormattingCodes[new Regex("wobbly")] = m => new AnimationCode(AnimationCode.Wobbly);
|
FormattingCodes[new Regex("wobbly")] = (m, i) => new AnimationCode(i, (state, batch, totalText, index, charSt, position, scale, layerDepth) => {
|
||||||
FormattingCodes[new Regex("typing")] = m => new AnimationCode(AnimationCode.Typing);
|
var code = (AnimationCode) state.FormattingCodes[i];
|
||||||
|
var offset = new Vector2(0, (float) Math.Sin(index + code.Time.TotalSeconds * state.Settings.WobbleModifier) * state.Font.LineHeight * state.Settings.WobbleHeightModifier * scale);
|
||||||
|
state.Font.DrawString(batch, charSt, position + offset, state.Color, 0, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
|
||||||
|
});
|
||||||
|
FormattingCodes[new Regex("typing")] = (m, i) => new AnimationCode(i, (state, batch, totalText, index, charSt, position, scale, layerDepth) => {
|
||||||
|
var code = (AnimationCode) state.FormattingCodes[i];
|
||||||
|
if (code.Time.TotalSeconds * state.Settings.TypingSpeed > index - i + 1)
|
||||||
|
state.Font.DrawString(batch, charSt, position, state.Color, 0, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetFormatIndicators(char opener, char closer) {
|
public static void SetFormatIndicators(char opener, char closer) {
|
||||||
|
@ -56,9 +65,15 @@ namespace MLEM.Formatting {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RemoveFormatting(this string s, IGenericFont font) {
|
public static string RemoveFormatting(this string s, IGenericFont font) {
|
||||||
|
var codeLengths = 0;
|
||||||
return formatRegex.Replace(s, match => {
|
return formatRegex.Replace(s, match => {
|
||||||
var code = FromMatch(match);
|
var code = FromMatch(match, match.Index - codeLengths);
|
||||||
return code != null ? code.GetReplacementString(font) : string.Empty;
|
if (code != null) {
|
||||||
|
var replace = code.GetReplacementString(font);
|
||||||
|
codeLengths += match.Length - replace.Length;
|
||||||
|
return replace;
|
||||||
|
}
|
||||||
|
return match.Value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,77 +81,51 @@ namespace MLEM.Formatting {
|
||||||
var codes = new FormattingCodeCollection();
|
var codes = new FormattingCodeCollection();
|
||||||
var codeLengths = 0;
|
var codeLengths = 0;
|
||||||
foreach (Match match in formatRegex.Matches(s)) {
|
foreach (Match match in formatRegex.Matches(s)) {
|
||||||
var code = FromMatch(match);
|
var index = match.Index - codeLengths;
|
||||||
|
var code = FromMatch(match, index);
|
||||||
if (code == null)
|
if (code == null)
|
||||||
continue;
|
continue;
|
||||||
codes[match.Index - codeLengths] = code;
|
codes[index] = code;
|
||||||
codeLengths += match.Length - code.GetReplacementString(font).Length;
|
codeLengths += match.Length - code.GetReplacementString(font).Length;
|
||||||
}
|
}
|
||||||
return codes;
|
return codes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DrawFormattedString(this IGenericFont regularFont, SpriteBatch batch, Vector2 pos, string text, FormattingCodeCollection codes, Color color, float scale, IGenericFont boldFont = null, IGenericFont italicFont = null, float depth = 0, FormatSettings formatSettings = null) {
|
public static void DrawFormattedString(this IGenericFont regularFont, SpriteBatch batch, Vector2 pos, string text, FormattingCodeCollection codes, Color color, float scale, IGenericFont boldFont = null, IGenericFont italicFont = null, float depth = 0, FormatSettings formatSettings = null) {
|
||||||
var settings = formatSettings ?? FormatSettings.Default;
|
var state = new FormatState {
|
||||||
var currColor = color;
|
FormattingCodes = codes,
|
||||||
var currFont = regularFont;
|
RegularFont = regularFont,
|
||||||
var currStyle = TextStyle.Regular;
|
ItalicFont = italicFont,
|
||||||
var currAnim = AnimationCode.DefaultCode;
|
BoldFont = boldFont,
|
||||||
var animStart = 0;
|
Settings = formatSettings ?? FormatSettings.Default,
|
||||||
|
|
||||||
|
Color = color,
|
||||||
|
Font = regularFont,
|
||||||
|
DrawBehavior = FormatState.DefaultDrawBehavior
|
||||||
|
};
|
||||||
|
|
||||||
var innerOffset = new Vector2();
|
|
||||||
for (var i = 0; i < text.Length; i++) {
|
for (var i = 0; i < text.Length; i++) {
|
||||||
// check if the current character's index has a formatting code
|
state.CurrentIndex = i;
|
||||||
codes.TryGetValue(i, out var code);
|
if (codes.TryGetValue(i, out var code))
|
||||||
if (code != null) {
|
code.ModifyFormatting(ref state);
|
||||||
// if so, apply it
|
|
||||||
switch (code.CodeType) {
|
|
||||||
case FormattingCode.Type.Color:
|
|
||||||
currColor = code.Color.CopyAlpha(color);
|
|
||||||
break;
|
|
||||||
case FormattingCode.Type.Style:
|
|
||||||
switch (code.Style) {
|
|
||||||
case TextStyle.Regular:
|
|
||||||
currFont = regularFont;
|
|
||||||
break;
|
|
||||||
case TextStyle.Bold:
|
|
||||||
currFont = boldFont ?? regularFont;
|
|
||||||
break;
|
|
||||||
case TextStyle.Italic:
|
|
||||||
currFont = italicFont ?? regularFont;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
currStyle = code.Style;
|
|
||||||
break;
|
|
||||||
case FormattingCode.Type.Icon:
|
|
||||||
batch.Draw(code.Icon.CurrentRegion, new RectangleF(pos + innerOffset, new Vector2(regularFont.LineHeight * scale)), color, 0, Vector2.Zero, SpriteEffects.None, depth);
|
|
||||||
break;
|
|
||||||
case FormattingCode.Type.Animation:
|
|
||||||
currAnim = (AnimationCode) code;
|
|
||||||
animStart = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var c = text[i];
|
var c = text[i];
|
||||||
var cSt = c.ToString();
|
var cSt = c.ToString();
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
innerOffset.X = 0;
|
state.InnerOffset = new Vector2(0, state.InnerOffset.Y + regularFont.LineHeight * scale);
|
||||||
innerOffset.Y += regularFont.LineHeight * scale;
|
|
||||||
} else {
|
} else {
|
||||||
if (currStyle == TextStyle.Shadow)
|
state.DrawBehavior(state, batch, text, i, cSt, pos + state.InnerOffset, scale, depth);
|
||||||
currAnim.Draw(currAnim, settings, currFont, batch, text, i, animStart, cSt, pos + innerOffset + settings.DropShadowOffset * scale, settings.DropShadowColor, scale, depth);
|
state.InnerOffset += new Vector2(regularFont.MeasureString(cSt).X * scale, 0);
|
||||||
currAnim.Draw(currAnim, settings, currFont, batch, text, i, animStart, cSt, pos + innerOffset, currColor, scale, depth);
|
|
||||||
innerOffset.X += regularFont.MeasureString(cSt).X * scale;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FormattingCode FromMatch(Capture match) {
|
private static FormattingCode FromMatch(Capture match, int index) {
|
||||||
var rawCode = match.Value.Substring(1, match.Value.Length - 2).ToLowerInvariant();
|
var rawCode = match.Value.Substring(1, match.Value.Length - 2).ToLowerInvariant();
|
||||||
foreach (var code in FormattingCodes) {
|
foreach (var code in FormattingCodes) {
|
||||||
var m = code.Key.Match(rawCode);
|
var m = code.Key.Match(rawCode);
|
||||||
if (m.Success)
|
if (m.Success)
|
||||||
return code.Value(m);
|
return code.Value(m, index);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue