Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Sprite/Sprite.cs
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Graphics;
3 using System;
4 using Barotrauma.IO;
5 using System.Linq;
6 using System.Collections.Generic;
7 using System.Threading.Tasks;
8 
9 namespace Barotrauma
10 {
11  public partial class Sprite
12  {
13  public Identifier Identifier { get; private set; }
14  public static IEnumerable<Sprite> LoadedSprites
15  {
16  get
17  {
18  List<Sprite> retVal = null;
19  lock (list)
20  {
21  retVal = list.Select(wRef =>
22  {
23  if (wRef.TryGetTarget(out Sprite spr))
24  {
25  return spr;
26  }
27  return null;
28  }).Where(s => s != null).ToList();
29  }
30  return retVal;
31  }
32  }
33 
34  private static readonly List<WeakReference<Sprite>> list = new List<WeakReference<Sprite>>();
35 
36  static partial void AddToList(Sprite elem)
37  {
38  lock (list)
39  {
40  list.Add(new WeakReference<Sprite>(elem));
41  }
42  }
43 
44  static partial void RemoveFromList(Sprite sprite)
45  {
46  lock (list)
47  {
48  list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == sprite);
49  }
50  }
51 
52  private class TextureRefCounter
53  {
54  public Texture2D Texture;
55  public int RefCount;
56  }
57 
58  private readonly static Dictionary<Identifier, TextureRefCounter> textureRefCounts = new Dictionary<Identifier, TextureRefCounter>();
59 
60  private bool cannotBeLoaded;
61 
62  protected volatile bool loadingAsync = false;
63 
64  protected Texture2D texture { get; private set; }
65  public Texture2D Texture
66  {
67  get
68  {
70  return texture;
71  }
72  }
73 
74  private string disposeStackTrace;
75 
76  public bool Loaded
77  {
78  get { return texture != null && !cannotBeLoaded; }
79  }
80 
81  public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation, other.FilePath.Value)
82  {
83  Compress = other.Compress;
84  size = other.size;
85  effects = other.effects;
86  }
87 
88  public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f, string path = null)
89  {
90  this.texture = texture;
91  sourceRect = sourceRectangle ?? new Rectangle(0, 0, texture.Width, texture.Height);
92  offset = newOffset ?? Vector2.Zero;
93  size = new Vector2(sourceRect.Width, sourceRect.Height);
94  origin = Vector2.Zero;
95  effects = SpriteEffects.None;
96  rotation = newRotation;
98  AddToList(this);
99  if (!string.IsNullOrEmpty(path))
100  {
101  Identifier fullPath = Path.GetFullPath(path).CleanUpPathCrossPlatform(correctFilenameCase: false).ToIdentifier();
102  lock (list)
103  {
104  if (!textureRefCounts.TryAdd(fullPath, new TextureRefCounter { RefCount = 1, Texture = texture }))
105  {
106  textureRefCounts[fullPath].RefCount++;
107  }
108  }
109  }
110  }
111 
112  partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn)
113  {
114  texture = LoadTexture(FilePath.Value, Compress, contentPackage: SourceElement?.ContentPackage);
115 
116  if (texture == null)
117  {
118  shouldReturn = true;
119  return;
120  }
121 
122  if (sourceVector.Z == 0.0f) sourceVector.Z = texture.Width;
123  if (sourceVector.W == 0.0f) sourceVector.W = texture.Height;
124  }
125 
126  public async Task LazyLoadAsync()
127  {
128  await Task.Yield();
129  if (!LazyLoad || texture != null || cannotBeLoaded || loadingAsync) { return; }
130  EnsureLazyLoaded(isAsync: true);
131  }
132 
133  public void EnsureLazyLoaded(bool isAsync = false)
134  {
135  if (!LazyLoad || texture != null || cannotBeLoaded || loadingAsync) { return; }
136  loadingAsync = isAsync;
137 
138  Vector4 sourceVector = Vector4.Zero;
139  bool temp2 = false;
140  int maxLoadRetries = File.Exists(FilePath) ? 3 : 0;
141  for (int i = 0; i <= maxLoadRetries; i++)
142  {
143  try
144  {
145  LoadTexture(ref sourceVector, ref temp2);
146  }
147  catch (System.IO.IOException)
148  {
149  if (i == maxLoadRetries || !File.Exists(FilePath)) { throw; }
150  DebugConsole.NewMessage("Loading sprite \"" + FilePath + "\" failed, retrying in 250 ms...");
151  System.Threading.Thread.Sleep(500);
152  }
153  }
154 
155  if (sourceRect.Width == 0 && sourceRect.Height == 0)
156  {
157  sourceRect = new Rectangle((int)sourceVector.X, (int)sourceVector.Y, (int)sourceVector.Z, (int)sourceVector.W);
158  size = SourceElement.GetAttributeVector2("size", Vector2.One);
159  size.X *= sourceRect.Width;
160  size.Y *= sourceRect.Height;
161  RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f));
162  }
163  if (texture == null)
164  {
165  cannotBeLoaded = true;
166  }
167  }
168 
169  public void ReloadTexture()
170  {
171  var oldTexture = texture;
172  if (texture == null)
173  {
174  DebugConsole.ThrowError("Sprite: Failed to reload the texture, texture is null.");
175  return;
176  }
177  texture.Dispose();
178  texture = TextureLoader.FromFile(FilePath.Value, Compress, contentPackage: SourceElement?.ContentPackage);
179  Identifier pathKey = FullPath.ToIdentifier();
180  if (textureRefCounts.ContainsKey(pathKey))
181  {
182  textureRefCounts[pathKey].Texture = texture;
183  }
184  foreach (Sprite sprite in LoadedSprites)
185  {
186  if (sprite.texture == oldTexture)
187  {
188  sprite.texture = texture;
189  }
190  }
191  }
192 
193  partial void CalculateSourceRect()
194  {
195  sourceRect = new Rectangle(0, 0, texture.Width, texture.Height);
196  }
197 
198  public static Texture2D LoadTexture(string file, bool compress = true, ContentPackage contentPackage = null)
199  {
200  if (string.IsNullOrWhiteSpace(file))
201  {
202  Texture2D t = null;
203  CrossThread.RequestExecutionOnMainThread(() =>
204  {
205  t = new Texture2D(GameMain.GraphicsDeviceManager.GraphicsDevice, 1, 1);
206  });
207  return t;
208  }
209  Identifier fullPath = Path.GetFullPath(file).CleanUpPathCrossPlatform(correctFilenameCase: false).ToIdentifier();
210  lock (list)
211  {
212  if (textureRefCounts.ContainsKey(fullPath))
213  {
214  textureRefCounts[fullPath].RefCount++;
215  return textureRefCounts[fullPath].Texture;
216  }
217  }
218 
219  if (File.Exists(file))
220  {
221  if (!ToolBox.IsProperFilenameCase(file))
222  {
223 #if DEBUG
224  DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!", contentPackage: contentPackage);
225 #endif
226  }
227 
228  Texture2D newTexture = TextureLoader.FromFile(file, compress, contentPackage: contentPackage);
229  lock (list)
230  {
231  if (!textureRefCounts.TryAdd(fullPath,
232  new TextureRefCounter { RefCount = 1, Texture = newTexture }))
233  {
234  CrossThread.RequestExecutionOnMainThread(() => newTexture.Dispose());
235  textureRefCounts[fullPath].RefCount++;
236  return textureRefCounts[fullPath].Texture;
237  }
238  }
239  return newTexture;
240  }
241  else
242  {
243  DebugConsole.ThrowError($"Sprite \"{file}\" not found!", contentPackage: contentPackage);
244  DebugConsole.Log(Environment.StackTrace.CleanupStackTrace());
245  }
246 
247  return null;
248  }
249 
250  public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None)
251  {
252  this.Draw(spriteBatch, pos, Color.White, rotate, scale, spriteEffect);
253  }
254 
255  public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null)
256  {
257  this.Draw(spriteBatch, pos, color, this.origin, rotate, new Vector2(scale, scale), spriteEffect, depth);
258  }
259 
260  public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null)
261  {
262  this.Draw(spriteBatch, pos, color, origin, rotate, new Vector2(scale, scale), spriteEffect, depth);
263  }
264 
265  public virtual void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null)
266  {
267  if (Texture == null) { return; }
268  //DrawSilhouette(spriteBatch, pos, origin, rotate, scale, spriteEffect, depth);
269  spriteBatch.Draw(texture, pos + offset, sourceRect, color, rotation + rotate, origin, scale, spriteEffect, depth ?? this.depth);
270  }
271 
275  public void DrawSilhouette(SpriteBatch spriteBatch, Vector2 pos, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null)
276  {
277  if (Texture == null) { return; }
278  for (int x = -1; x <= 1; x += 2)
279  {
280  for (int y = -1; y <= 1; y += 2)
281  {
282  spriteBatch.Draw(texture, pos + offset + new Vector2(x, y), sourceRect, Color.Black, rotation + rotate, origin, scale, spriteEffect, (depth ?? this.depth) + 0.01f);
283  }
284  }
285  }
286 
290  public static readonly Version LastBrokenTiledSpriteGameVersion = new Version(major: 1, minor: 2, build: 7, revision: 0);
291 
292  public void DrawTiled(ISpriteBatch spriteBatch,
293  Vector2 position,
294  Vector2 targetSize,
295  float rotation = 0f,
296  Vector2? origin = null,
297  Color? color = null,
298  Vector2? startOffset = null,
299  Vector2? textureScale = null,
300  float? depth = null,
301  SpriteEffects? spriteEffects = null)
302  {
303  if (Texture == null) { return; }
304 
305  spriteEffects ??= effects;
306 
307  bool flipHorizontal = spriteEffects.Value.HasFlag(SpriteEffects.FlipHorizontally);
308  bool flipVertical = spriteEffects.Value.HasFlag(SpriteEffects.FlipVertically);
309 
310  float addedRotation = rotation + this.rotation;
311  if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; }
312 
313  Vector2 advanceX = addedRotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(addedRotation), (float)Math.Sin(addedRotation));
314  Vector2 advanceY = new Vector2(-advanceX.Y, advanceX.X);
315 
316  //Init optional values
317  Vector2 drawOffset = startOffset ?? Vector2.Zero;
318  Vector2 scale = textureScale ?? Vector2.One;
319  Color drawColor = color ?? Color.White;
320  Vector2 transformedOrigin = origin ?? Vector2.Zero;
321 
322  transformedOrigin = advanceX * transformedOrigin.X + advanceY * transformedOrigin.Y;
323 
324  void drawSection(Vector2 slicePos, Rectangle sliceRect)
325  {
326  Vector2 transformedPos = slicePos;
327 
328  if (flipHorizontal)
329  {
330  transformedPos.X = targetSize.X - transformedPos.X - sliceRect.Width * scale.X;
331  }
332  if (flipVertical)
333  {
334  transformedPos.Y = targetSize.Y - transformedPos.Y - sliceRect.Height * scale.Y;
335  }
336 
337  transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y;
338  transformedPos += position - transformedOrigin;
339  spriteBatch.Draw(
340  texture: texture,
341  position: transformedPos,
342  sourceRectangle: sliceRect,
343  color: drawColor,
344  rotation: addedRotation,
345  origin: Vector2.Zero,
346  scale: scale,
347  effects: spriteEffects.Value,
348  layerDepth: depth ?? this.depth);
349  }
350 
351  //wrap the drawOffset inside the sourceRect
352  drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width;
353  drawOffset.Y = (drawOffset.Y / scale.Y) % sourceRect.Height;
354 
355  //how many times the texture needs to be drawn on the x-axis
356  int xTiles = (int)Math.Ceiling((targetSize.X + drawOffset.X * scale.X) / (sourceRect.Width * scale.X));
357  //how many times the texture needs to be drawn on the y-axis
358  int yTiles = (int)Math.Ceiling((targetSize.Y + drawOffset.Y * scale.Y) / (sourceRect.Height * scale.Y));
359 
360  //where the current tile is being drawn;
361  Vector2 currDrawPosition = -drawOffset;
362  //which part of the texture we are currently drawing
363  Rectangle texPerspective = sourceRect;
364 
365 
366  for (int x = 0; x < xTiles; x++)
367  {
368  texPerspective.X = sourceRect.X;
369  texPerspective.Width = sourceRect.Width;
370  texPerspective.Height = sourceRect.Height;
371 
372  //offset to the left, draw a partial slice
373  if (currDrawPosition.X < 0)
374  {
375  float diff = -currDrawPosition.X;
376  currDrawPosition.X += diff;
377  texPerspective.Width -= (int)diff;
378 
379  texPerspective.X += (int)diff;
380  }
381  //make sure the rightmost tiles don't go over the right side
382  if (x == xTiles - 1)
383  {
384  int diff = (int)(((currDrawPosition.X + texPerspective.Width * scale.X) - targetSize.X) / scale.X);
385  texPerspective.Width -= diff;
386  }
387 
388  currDrawPosition.Y = -drawOffset.Y;
389 
390  for (int y = 0; y < yTiles; y++)
391  {
392  texPerspective.Y = sourceRect.Y;
393  texPerspective.Height = sourceRect.Height;
394 
395  //offset above the top, draw a partial slice
396  if (currDrawPosition.Y < 0f)
397  {
398  float diff = -currDrawPosition.Y;
399  currDrawPosition.Y += diff;
400  texPerspective.Height -= (int)diff;
401  texPerspective.Y += (int)diff;
402  }
403 
404  //make sure the bottommost tiles don't go over the bottom
405  if (y == yTiles - 1)
406  {
407  int diff = (int)(((currDrawPosition.Y + texPerspective.Height * scale.Y) - targetSize.Y) / scale.Y);
408  texPerspective.Height -= diff;
409  }
410 
411  drawSection(currDrawPosition, texPerspective);
412 
413  currDrawPosition.Y += texPerspective.Height * scale.Y;
414  }
415 
416  currDrawPosition.X += texPerspective.Width * scale.X;
417  }
418  }
419 
420  partial void DisposeTexture()
421  {
422  disposeStackTrace = Environment.StackTrace;
423  if (texture != null)
424  {
425  //check if another sprite is using the same texture
426  lock (list)
427  {
428  if (!FilePath.IsNullOrEmpty()) //file can be empty if the sprite is created directly from a Texture2D instance
429  {
430  Identifier pathKey = FullPath.ToIdentifier();
431  if (!pathKey.IsEmpty && textureRefCounts.ContainsKey(pathKey))
432  {
433  textureRefCounts[pathKey].RefCount--;
434  if (textureRefCounts[pathKey].RefCount <= 0)
435  {
436  textureRefCounts.Remove(pathKey);
437  }
438  else
439  {
440  texture = null;
441  FilePath = ContentPath.Empty;
442  return;
443  }
444  }
445  }
446  }
447 
448  //if not, free the texture
449  CrossThread.RequestExecutionOnMainThread(() =>
450  {
451  texture.Dispose();
452  });
453  texture = null;
454  }
455  }
456  }
457 }
458 
static ContentPath FromRaw(string? rawValue)
static readonly ContentPath Empty
Definition: ContentPath.cs:12
string???????????? Value
Definition: ContentPath.cs:27
ContentPackage? ContentPackage
Vector2 GetAttributeVector2(string key, in Vector2 def)
static GraphicsDeviceManager GraphicsDeviceManager
Definition: GameMain.cs:150
virtual void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect=SpriteEffects.None, float? depth=null)
void DrawSilhouette(SpriteBatch spriteBatch, Vector2 pos, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect=SpriteEffects.None, float? depth=null)
Creates a silhouette for the sprite (or outline if the sprite is rendered on top of it)
void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None, float? depth=null)
static readonly Version LastBrokenTiledSpriteGameVersion
Last version of the game that had broken handling of sprites that were scaled, flipped and offset
void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None, float? depth=null)
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation=0.0f, string path=null)
Sprite(ContentXElement element, string path="", string file="", bool lazyLoad=false, float sourceRectScale=1)
ContentXElement SourceElement
Reference to the xml element from where the sprite was created. Can be null if the sprite was not def...
static IEnumerable< Sprite > LoadedSprites
void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, float rotation=0f, Vector2? origin=null, Color? color=null, Vector2? startOffset=null, Vector2? textureScale=null, float? depth=null, SpriteEffects? spriteEffects=null)
static Texture2D LoadTexture(string file, bool compress=true, ContentPackage contentPackage=null)