zc
This commit is contained in:
549
Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
Normal file
549
Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
Normal file
@@ -0,0 +1,549 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
using WulaFallenEmpire.EventSystem.AI;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||
{
|
||||
public class Overlay_WulaLink : Window
|
||||
{
|
||||
// Core Connection
|
||||
private AIIntelligenceCore _core;
|
||||
private string _eventDefName;
|
||||
private EventDef _def;
|
||||
|
||||
// UI State
|
||||
private Vector2 _scrollPosition = Vector2.zero;
|
||||
private string _inputText = "";
|
||||
private bool _scrollToBottom = true;
|
||||
|
||||
|
||||
// HUD / Minimized State
|
||||
private bool _isMinimized = false;
|
||||
private int _unreadCount = 0;
|
||||
private Vector2 _expandedSize;
|
||||
private Vector2 _minimizedSize = new Vector2(220f, 80f);
|
||||
|
||||
// Layout Constants
|
||||
private const float HeaderHeight = 50f;
|
||||
private const float FooterHeight = 50f;
|
||||
private const float AvatarSize = 40f;
|
||||
private const float BubblePadding = 10f;
|
||||
private const float MessageSpacing = 15f;
|
||||
private const float MaxBubbleWidthRatio = 0.75f;
|
||||
|
||||
public Overlay_WulaLink(EventDef def)
|
||||
{
|
||||
this._def = def;
|
||||
this._eventDefName = def.defName;
|
||||
|
||||
// Window Properties (Floating, Non-Modal)
|
||||
this.layer = WindowLayer.GameUI;
|
||||
this.forcePause = false;
|
||||
this.absorbInputAroundWindow = false;
|
||||
this.closeOnClickedOutside = false;
|
||||
this.doWindowBackground = false; // We draw our own
|
||||
this.drawShadow = true;
|
||||
this.draggable = true;
|
||||
this.resizeable = true;
|
||||
this.preventCameraMotion = false;
|
||||
|
||||
// Initial Size (Phone-like)
|
||||
_expandedSize = new Vector2(380f, 600f);
|
||||
}
|
||||
|
||||
public override Vector2 InitialSize => _isMinimized ? _minimizedSize : _expandedSize;
|
||||
|
||||
public void ToggleMinimize()
|
||||
{
|
||||
_isMinimized = !_isMinimized;
|
||||
_unreadCount = 0; // Reset on toggle? Or only on expand? Let's say expand.
|
||||
|
||||
if (_isMinimized)
|
||||
{
|
||||
// Save current position if needed, or just snap to right edge
|
||||
windowRect.width = _minimizedSize.x;
|
||||
windowRect.height = _minimizedSize.y;
|
||||
windowRect.x = Verse.UI.screenWidth - _minimizedSize.x - 20f;
|
||||
windowRect.y = Verse.UI.screenHeight / 2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
windowRect.width = _expandedSize.x;
|
||||
windowRect.height = _expandedSize.y;
|
||||
}
|
||||
}
|
||||
|
||||
public void Expand()
|
||||
{
|
||||
if (_isMinimized) ToggleMinimize();
|
||||
// Ensure window is on screen
|
||||
if (windowRect.x > Verse.UI.screenWidth - 100f)
|
||||
{
|
||||
windowRect.x = Verse.UI.screenWidth - _expandedSize.x - 20f;
|
||||
}
|
||||
Find.WindowStack.Notify_ManuallySetFocus(this);
|
||||
}
|
||||
|
||||
public override void PreOpen()
|
||||
{
|
||||
base.PreOpen();
|
||||
// Connect to Core
|
||||
_core = Find.World.GetComponent<AIIntelligenceCore>();
|
||||
if (_core != null)
|
||||
{
|
||||
_core.InitializeConversation(_eventDefName);
|
||||
_core.OnMessageReceived += OnMessageReceived;
|
||||
_core.OnThinkingStateChanged += OnThinkingStateChanged;
|
||||
_core.OnExpressionChanged += OnExpressionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostClose()
|
||||
{
|
||||
base.PostClose();
|
||||
if (_core != null)
|
||||
{
|
||||
_core.OnMessageReceived -= OnMessageReceived;
|
||||
_core.OnThinkingStateChanged -= OnThinkingStateChanged;
|
||||
_core.OnExpressionChanged -= OnExpressionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessageReceived(string msg)
|
||||
{
|
||||
_scrollToBottom = true;
|
||||
if (_isMinimized)
|
||||
{
|
||||
_unreadCount++;
|
||||
// Spawn Notification Bubble
|
||||
Find.WindowStack.Add(new Overlay_WulaLink_Notification(msg));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThinkingStateChanged(bool thinking)
|
||||
{
|
||||
// Trigger repaint or animation update if needed
|
||||
}
|
||||
|
||||
private void OnExpressionChanged(int id)
|
||||
{
|
||||
// Repaint happens next frame
|
||||
}
|
||||
|
||||
public override void DoWindowContents(Rect inRect)
|
||||
{
|
||||
if (_isMinimized)
|
||||
{
|
||||
DrawMinimized(inRect);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw Main Background (Whole Window)
|
||||
Widgets.DrawBoxSolid(inRect, WulaLinkStyles.BackgroundColor);
|
||||
GUI.color = new Color(0.8f, 0.8f, 0.8f);
|
||||
Widgets.DrawBox(inRect, 1); // Border
|
||||
GUI.color = Color.white;
|
||||
|
||||
// Areas
|
||||
Rect headerRect = new Rect(0, 0, inRect.width, HeaderHeight);
|
||||
Rect footerRect = new Rect(0, inRect.height - FooterHeight, inRect.width, FooterHeight);
|
||||
Rect contextRect = new Rect(0, inRect.height - FooterHeight - 30f, inRect.width, 30f); // Context Bar
|
||||
Rect bodyRect = new Rect(0, HeaderHeight, inRect.width, inRect.height - HeaderHeight - FooterHeight - 30f);
|
||||
|
||||
// Draw Components
|
||||
DrawHeader(headerRect);
|
||||
DrawMessageList(bodyRect);
|
||||
DrawContextBar(contextRect);
|
||||
DrawFooter(footerRect);
|
||||
}
|
||||
|
||||
private void DrawMinimized(Rect rect)
|
||||
{
|
||||
// HUD Capsule Style
|
||||
Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.85f)); // Semi-transparent black
|
||||
GUI.color = WulaLinkStyles.HeaderColor;
|
||||
Widgets.DrawBox(rect, 2); // Thicker colored border
|
||||
GUI.color = Color.white;
|
||||
|
||||
// Layout
|
||||
Rect titleRect = new Rect(rect.x + 10f, rect.y + 5f, rect.width - 20f, 25f);
|
||||
Rect statusRect = new Rect(rect.x + 10f, rect.yMax - 30f, rect.width - 20f, 25f);
|
||||
|
||||
// Title
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Text.Font = GameFont.Small;
|
||||
Widgets.Label(titleRect, "WULA LINK");
|
||||
|
||||
// Status
|
||||
string status = _core.IsThinking ? "Thinking..." : "Standby";
|
||||
Color statusColor = _core.IsThinking ? Color.yellow : Color.green;
|
||||
|
||||
GUI.color = statusColor;
|
||||
Widgets.Label(statusRect, $"â—?{status}");
|
||||
GUI.color = Color.white;
|
||||
|
||||
// Unread Badge
|
||||
if (_unreadCount > 0)
|
||||
{
|
||||
float badgeSize = 24f;
|
||||
Rect badgeRect = new Rect(rect.xMax - badgeSize - 5f, rect.y - 10f, badgeSize, badgeSize);
|
||||
GUI.color = Color.red;
|
||||
GUI.DrawTexture(badgeRect, BaseContent.WhiteTex); // Circle ideally
|
||||
GUI.color = Color.white;
|
||||
Text.Font = GameFont.Tiny;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(badgeRect, _unreadCount.ToString());
|
||||
Text.Font = GameFont.Small;
|
||||
}
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
|
||||
// Click to Expand
|
||||
if (Widgets.ButtonInvisible(rect))
|
||||
{
|
||||
ToggleMinimize();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawContextBar(Rect rect)
|
||||
{
|
||||
// Context Awareness
|
||||
Widgets.DrawBoxSolid(rect, new Color(0.15f, 0.15f, 0.15f, 1f));
|
||||
GUI.color = Color.grey;
|
||||
Widgets.DrawLineHorizontal(rect.x, rect.y, rect.width);
|
||||
|
||||
string contextInfo = "Context: None";
|
||||
if (Find.Selector.SingleSelectedThing != null)
|
||||
{
|
||||
contextInfo = $"Context: [{Find.Selector.SingleSelectedThing.LabelCap}]";
|
||||
}
|
||||
else if (Find.Selector.SelectedObjects.Count > 1)
|
||||
{
|
||||
contextInfo = $"Context: {Find.Selector.SelectedObjects.Count} objects selected";
|
||||
}
|
||||
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
Text.Font = GameFont.Tiny;
|
||||
Widgets.Label(new Rect(rect.x + 10f, rect.y, rect.width - 20f, rect.height), contextInfo);
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private void DrawHeader(Rect rect)
|
||||
{
|
||||
// Header BG
|
||||
Widgets.DrawBoxSolid(rect, WulaLinkStyles.HeaderColor);
|
||||
|
||||
// Title
|
||||
Text.Font = GameFont.Medium;
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
GUI.color = Color.white;
|
||||
Rect titleRect = rect;
|
||||
titleRect.x += 10f;
|
||||
Widgets.Label(titleRect, _def.characterName ?? "MomoTalk");
|
||||
|
||||
// Header Icons (Minimize/Close)
|
||||
Rect closeRect = new Rect(rect.width - 30f, 10f, 20f, 20f);
|
||||
Rect minRect = new Rect(rect.width - 60f, 10f, 20f, 20f);
|
||||
|
||||
if (Widgets.ButtonText(minRect, "-"))
|
||||
{
|
||||
ToggleMinimize();
|
||||
}
|
||||
|
||||
if (Widgets.ButtonImage(closeRect, Widgets.CheckboxOffTex)) // Use standard X tex
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
GUI.color = Color.white;
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
}
|
||||
|
||||
private void DrawMessageList(Rect rect)
|
||||
{
|
||||
var history = _core?.GetHistorySnapshot();
|
||||
if (history == null) return;
|
||||
|
||||
// Filter out tool messages for cleaner display (only show in DevMode)
|
||||
var displayHistory = new List<(string role, string message)>();
|
||||
foreach (var msg in history)
|
||||
{
|
||||
// Skip tool/toolcall messages entirely in normal mode
|
||||
if ((msg.role == "tool" || msg.role == "toolcall") && !Prefs.DevMode) continue;
|
||||
displayHistory.Add(msg);
|
||||
}
|
||||
|
||||
// Setup ScrollView
|
||||
float contentHeight = 0f;
|
||||
float width = rect.width - 26f; // Scrollbar space (16 + margin)
|
||||
float reducedSpacing = 8f; // Reduced from MessageSpacing (15f) to 8f
|
||||
|
||||
List<float> heights = new List<float>();
|
||||
foreach (var msg in displayHistory)
|
||||
{
|
||||
float h = CalcMessageHeight(msg.message, width);
|
||||
heights.Add(h);
|
||||
contentHeight += h + reducedSpacing;
|
||||
}
|
||||
|
||||
if (_core.IsThinking)
|
||||
{
|
||||
contentHeight += 40f; // Space for thinking indicator
|
||||
}
|
||||
|
||||
Rect viewRect = new Rect(0, 0, width, contentHeight);
|
||||
|
||||
// Handle Auto Scroll
|
||||
if (_scrollToBottom)
|
||||
{
|
||||
_scrollPosition.y = contentHeight - rect.height;
|
||||
_scrollToBottom = false;
|
||||
}
|
||||
|
||||
Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect);
|
||||
|
||||
float curY = 10f;
|
||||
for (int i = 0; i < displayHistory.Count; i++)
|
||||
{
|
||||
var entry = displayHistory[i];
|
||||
float h = heights[i];
|
||||
Rect msgRect = new Rect(0, curY, width, h);
|
||||
|
||||
if (entry.role == "user")
|
||||
{
|
||||
DrawSenseiMessage(msgRect, entry.message);
|
||||
}
|
||||
else if (entry.role == "assistant")
|
||||
{
|
||||
DrawStudentMessage(msgRect, entry.message);
|
||||
}
|
||||
else if (entry.role == "tool" || entry.role == "toolcall")
|
||||
{
|
||||
// Only shown in DevMode (already filtered above)
|
||||
DrawSystemMessage(msgRect, $"[Tool] {entry.message}");
|
||||
}
|
||||
else if (entry.role == "system")
|
||||
{
|
||||
DrawSystemMessage(msgRect, entry.message);
|
||||
}
|
||||
|
||||
curY += h + reducedSpacing;
|
||||
}
|
||||
|
||||
if (_core.IsThinking)
|
||||
{
|
||||
Rect thinkingRect = new Rect(0, curY, width, 30f);
|
||||
DrawThinkingIndicator(thinkingRect);
|
||||
}
|
||||
|
||||
Widgets.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawFooter(Rect rect)
|
||||
{
|
||||
Widgets.DrawBoxSolid(rect, WulaLinkStyles.InputBarColor);
|
||||
Widgets.DrawLineHorizontal(rect.x, rect.y, rect.width); // Top border
|
||||
|
||||
float padding = 8f;
|
||||
float btnWidth = 40f;
|
||||
float inputWidth = rect.width - btnWidth - (padding * 3);
|
||||
|
||||
Rect inputRect = new Rect(rect.x + padding, rect.y + padding, inputWidth, rect.height - (padding * 2));
|
||||
Rect btnRect = new Rect(inputRect.xMax + padding, rect.y + padding, btnWidth, rect.height - (padding * 2));
|
||||
|
||||
// Input Field
|
||||
string nextInput = Widgets.TextField(inputRect, _inputText);
|
||||
if (nextInput != _inputText)
|
||||
{
|
||||
_inputText = nextInput;
|
||||
}
|
||||
|
||||
// Send Button (Simulate Enter key or Click)
|
||||
bool enterPressed = (Event.current.type == EventType.KeyDown && (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && GUI.GetNameOfFocusedControl() == "WulaInput");
|
||||
|
||||
bool sendClicked = DrawCustomButton(btnRect, ">", !string.IsNullOrWhiteSpace(_inputText));
|
||||
if (sendClicked || enterPressed)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_inputText))
|
||||
{
|
||||
bool wasFocused = GUI.GetNameOfFocusedControl() == "WulaInput";
|
||||
_core.SendUserMessage(_inputText);
|
||||
_inputText = "";
|
||||
if (wasFocused) GUI.FocusControl("WulaInput");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle focus for Enter key to work
|
||||
if (Mouse.IsOver(inputRect) && Event.current.type == EventType.MouseDown)
|
||||
{
|
||||
GUI.FocusControl("WulaInput");
|
||||
}
|
||||
GUI.SetNextControlName("WulaInput");
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// Message Rendering Helpers
|
||||
// =================================================================================
|
||||
|
||||
private float CalcMessageHeight(string text, float containerWidth)
|
||||
{
|
||||
float maxBubbleWidth = containerWidth * MaxBubbleWidthRatio;
|
||||
Text.Font = WulaLinkStyles.MessageFont;
|
||||
float textH = Text.CalcHeight(text, maxBubbleWidth - (BubblePadding * 2));
|
||||
return Mathf.Max(textH + (BubblePadding * 2), AvatarSize);
|
||||
}
|
||||
|
||||
private void DrawSenseiMessage(Rect rect, string text)
|
||||
{
|
||||
// Right aligned Blue Bubble
|
||||
float maxBubbleWidth = rect.width * MaxBubbleWidthRatio;
|
||||
Text.Font = WulaLinkStyles.MessageFont;
|
||||
Vector2 textSize = Text.CalcSize(text);
|
||||
float bubbleWidth = Mathf.Min(textSize.x + (BubblePadding * 2), maxBubbleWidth);
|
||||
float bubbleHeight = rect.height;
|
||||
|
||||
Rect bubbleRect = new Rect(rect.xMax - bubbleWidth - 10f, rect.y, bubbleWidth, bubbleHeight);
|
||||
|
||||
// Draw Bubble
|
||||
GUI.color = WulaLinkStyles.SenseiBubbleColor;
|
||||
Widgets.DrawBoxSolid(bubbleRect, WulaLinkStyles.SenseiBubbleColor); // Rounded rect ideally
|
||||
GUI.color = Color.white;
|
||||
|
||||
// Text
|
||||
Rect textRect = bubbleRect.ContractedBy(BubblePadding);
|
||||
GUI.color = WulaLinkStyles.SenseiTextColor;
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
Widgets.Label(textRect, text);
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private void DrawStudentMessage(Rect rect, string text)
|
||||
{
|
||||
// Left aligned White Bubble + Avatar
|
||||
float avatarX = 10f;
|
||||
Rect avatarRect = new Rect(avatarX, rect.y, AvatarSize, AvatarSize);
|
||||
|
||||
// Avatar
|
||||
int expId = _core?.ExpressionId ?? 1;
|
||||
string portraitPath = _def.portraitPath ?? $"Wula/Events/Portraits/WULA_Legion_{expId}";
|
||||
if (expId > 1 && _def.portraitPath == null) // If using default Legion set
|
||||
{
|
||||
portraitPath = $"Wula/Events/Portraits/WULA_Legion_{expId}";
|
||||
}
|
||||
|
||||
Texture2D portrait = ContentFinder<Texture2D>.Get(portraitPath, false);
|
||||
if (portrait != null)
|
||||
{
|
||||
GUI.DrawTexture(avatarRect, portrait); // Needs circle mask ideally
|
||||
}
|
||||
else
|
||||
{
|
||||
Widgets.DrawBoxSolid(avatarRect, Color.gray);
|
||||
}
|
||||
|
||||
float maxBubbleWidth = rect.width * MaxBubbleWidthRatio;
|
||||
float bubbleX = avatarRect.xMax + 10f;
|
||||
|
||||
Text.Font = WulaLinkStyles.MessageFont;
|
||||
Vector2 textSize = Text.CalcSize(text);
|
||||
float bubbleWidth = Mathf.Min(textSize.x + (BubblePadding * 2), maxBubbleWidth);
|
||||
float bubbleHeight = rect.height; // Height was pre-calculated
|
||||
|
||||
Rect bubbleRect = new Rect(bubbleX, rect.y, bubbleWidth, bubbleHeight);
|
||||
|
||||
// Draw Bubble
|
||||
GUI.color = WulaLinkStyles.StudentStrokeColor;
|
||||
Widgets.DrawBox(bubbleRect, 1);
|
||||
GUI.color = WulaLinkStyles.StudentBubbleColor;
|
||||
Widgets.DrawBoxSolid(bubbleRect, WulaLinkStyles.StudentBubbleColor);
|
||||
GUI.color = Color.white;
|
||||
|
||||
// Text
|
||||
Rect textRect = bubbleRect.ContractedBy(BubblePadding);
|
||||
GUI.color = WulaLinkStyles.StudentTextColor;
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
Widgets.Label(textRect, text);
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private void DrawSystemMessage(Rect rect, string text)
|
||||
{
|
||||
// Centered gray text or Pink box
|
||||
if (text.Contains("[Tool]")) return; // Skip logic log in main view if needed, but here we draw minimal
|
||||
|
||||
GUI.color = Color.gray;
|
||||
Text.Font = GameFont.Tiny;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(rect, text);
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private void DrawThinkingIndicator(Rect rect)
|
||||
{
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
GUI.color = Color.gray;
|
||||
Rect iconRect = new Rect(rect.x + 60f, rect.y, 24f, 24f);
|
||||
Rect labelRect = new Rect(iconRect.xMax + 5f, rect.y, 200f, 24f);
|
||||
|
||||
// Draw a simple box as thinking indicator if TexUI is missing
|
||||
Widgets.DrawBoxSolid(iconRect, Color.gray);
|
||||
Widgets.Label(labelRect, "P.I.A is thinking...");
|
||||
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private bool DrawCustomButton(Rect rect, string label, bool isEnabled)
|
||||
{
|
||||
bool isMouseOver = Mouse.IsOver(rect);
|
||||
Color originalColor = GUI.color;
|
||||
TextAnchor originalAnchor = Text.Anchor;
|
||||
GameFont originalFont = Text.Font;
|
||||
|
||||
Color buttonColor;
|
||||
Color textColor;
|
||||
if (!isEnabled)
|
||||
{
|
||||
buttonColor = Dialog_CustomDisplay.CustomButtonDisabledColor;
|
||||
textColor = Dialog_CustomDisplay.CustomButtonTextDisabledColor;
|
||||
}
|
||||
else if (isMouseOver)
|
||||
{
|
||||
buttonColor = Dialog_CustomDisplay.CustomButtonHoverColor;
|
||||
textColor = Dialog_CustomDisplay.CustomButtonTextHoverColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonColor = Dialog_CustomDisplay.CustomButtonNormalColor;
|
||||
textColor = Dialog_CustomDisplay.CustomButtonTextNormalColor;
|
||||
}
|
||||
|
||||
GUI.color = buttonColor;
|
||||
Widgets.DrawBoxSolid(rect, buttonColor);
|
||||
Widgets.DrawBox(rect, 1);
|
||||
|
||||
GUI.color = textColor;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Text.Font = GameFont.Tiny;
|
||||
Widgets.Label(rect.ContractedBy(4f), label);
|
||||
|
||||
GUI.color = originalColor;
|
||||
Text.Anchor = originalAnchor;
|
||||
Text.Font = originalFont;
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Widgets.ButtonInvisible(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user