Add workshop things

This commit is contained in:
2025-03-28 18:56:25 +01:00
parent 0e3240cfbd
commit 16e40e6f2c
168 changed files with 26187 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="CsForBarotrauma" steamworkshopid="2795927223" corepackage="false" modversion="1.0.1" gameversion="0.17.12.0" installtime="1650551414" altnames="CsForBarotrauma" expectedhash="D41D8CD98F00B204E9800998ECF8427E" />

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Items>
<override>
<Item name="" identifier="endocrinebooster" category="Medical" maxstacksize="8" Tags="smallitem,chem,medical,syringe" allowasextracargo="true" description="" useinhealthinterface="true" scale="0.5" impactsoundtag="impact_metal_light">
<PreferredContainer primary="medcab" />
<Price baseprice="1400" sold="false">
<Price storeidentifier="merchantoutpost" />
<Price storeidentifier="merchantcity" multiplier="0.9" />
<Price storeidentifier="merchantresearch" multiplier="1.1" />
<Price storeidentifier="merchantmilitary" multiplier="1.1" />
<Price storeidentifier="merchantmine" />
</Price>
<Fabricate suitablefabricators="medicalfabricator" requiredtime="50" amount="1" requiresrecipe="true">
<RequiredSkill identifier="medical" level="90" />
<RequiredItem identifier="sulphuriteshard" />
<RequiredItem identifier="sulphuriteshard" />
<RequiredItem identifier="paralyxis" />
<RequiredItem identifier="paralyxis" />
<RequiredItem identifier="deusizine" />
</Fabricate>
<Deconstruct time="25">
<Item identifier="sulphuricacid" />
<Item identifier="sulphuricacid" />
<Item identifier="paralyxis" />
<Item identifier="deusizine" />
</Deconstruct>
<InventoryIcon texture="Content/Items/Medical/Medicines.png" sourcerect="448,256,64,64" origin="0.5,0.5" />
<Sprite texture="Content/Items/Medical/Medicines.png" sourcerect="303,310,35,67" depth="0.6" origin="0.5,0.5" />
<Body width="35" height="65" density="15" />
<MeleeWeapon canBeCombined="true" removeOnCombined="true" slots="Any,RightHand,LeftHand" aimpos="40,5" handle1="-5,0" holdangle="220" reload="1.0" msg="ItemMsgPickUpSelect">
<RequiredSkill identifier="medical" level="35" />
<StatusEffect type="OnSuccess" target="This" Condition="-100.0" disabledeltatime="true">
<Sound file="Content/Items/Medical/Syringe.ogg" range="500" />
</StatusEffect>
<StatusEffect type="OnFailure" target="This" Condition="-100.0" disabledeltatime="true">
<Sound file="Content/Items/Medical/Syringe.ogg" range="500" />
</StatusEffect>
<StatusEffect tags="medical" type="OnSuccess" target="Character">
<GiveTalentInfo giverandom="true" talentidentifiers="bountyhunter,logisticsexpert,prodigy,inspirationalleader,veteran,travelingtradesman,emergencymaneuvers,sailorwithnoname,camaraderie,downwiththeship,gunsmith,steadytune,protectandserve,warstories,firstaidtraining,physicalconditioning,swole,buff,weaponsmith,bythebook,dontpushit,bootcamp,munitionsexpertise,playingcatchup,mailman,skedaddle,crewlayabout,revengesquad,inspiringtunes,jackinthebox,peerlearning,stationengineer,junctionjunkie,egghead,grounded,remotemonitor,funwithfission,melodicrespite,submarineofthings,aggressiveengineering,samplecollection,salvagecrew,machinemaniac,safetyfirst,multifunctional,engineengineer,ballastdenizen,modularrepairs,oiledmachinery,pumpndump,ironman,retrofit,healthinsurance,nobodyimportantdies,exampleofhealth,laresistance,selfcare,stayinalive,firemanscarry,medicalcompanion,dontdieonme,nopressure,blooddonor" />
</StatusEffect>
<StatusEffect tags="medical" type="OnFailure" target="Character">
<GiveTalentInfo giverandom="true" talentidentifiers="bountyhunter,logisticsexpert,prodigy,inspirationalleader,veteran,travelingtradesman,emergencymaneuvers,sailorwithnoname,camaraderie,downwiththeship,gunsmith,steadytune,protectandserve,warstories,firstaidtraining,physicalconditioning,swole,buff,weaponsmith,bythebook,dontpushit,bootcamp,munitionsexpertise,playingcatchup,mailman,skedaddle,crewlayabout,revengesquad,inspiringtunes,jackinthebox,peerlearning,stationengineer,junctionjunkie,egghead,grounded,remotemonitor,funwithfission,melodicrespite,submarineofthings,aggressiveengineering,samplecollection,salvagecrew,machinemaniac,safetyfirst,multifunctional,engineengineer,ballastdenizen,modularrepairs,oiledmachinery,pumpndump,ironman,retrofit,healthinsurance,nobodyimportantdies,exampleofhealth,laresistance,selfcare,stayinalive,firemanscarry,medicalcompanion,dontdieonme,nopressure,blooddonor" />
</StatusEffect>
<StatusEffect type="OnBroken" target="This">
<Remove />
</StatusEffect>
</MeleeWeapon>
<Projectile characterusable="false" launchimpulse="20.0" sticktocharacters="true" launchrotation="-90" inheritstatuseffectsfrom="MeleeWeapon" inheritrequiredskillsfrom="MeleeWeapon" />
</Item>
</override>
</Items>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="All The Talent Trees + Unlock Parallel Talents" steamworkshopid="2913302583" corepackage="false" modversion="1.8" gameversion="1.7.7.0" installtime="1741741350" expectedhash="F618C637659A402B567063A51350B000">
<TalentTrees file="%ModDir%/TalentTrees.xml" />
</contentpackage>

View File

@@ -0,0 +1,323 @@
using Barotrauma.Items.Components;
using Barotrauma;
using HarmonyLib;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System;
using System.ComponentModel;
using Barotrauma.Networking;
using System.Linq;
namespace BaroMod_sjx
{
partial class ItemBoxImpl
{
[HarmonyPatch(typeof(Inventory), nameof(Inventory.DrawSlot))]
class Patch_DrawSlot
{
public class context
{
public SpriteBatch spriteBatch;
public Inventory inventory;
public Sprite? indicatorSprite;
public Sprite? emptyIndicatorSprite;
public Sprite? itemSprite;
public Rectangle conditionIndicatorArea;
public int max_value;
public int cur_value;
public Vector2 sprite_pos;
public float sprite_scale;
public float rotation;
public Color spriteColor;
public context(SpriteBatch sb, Inventory inv, Sprite? full, Sprite? empty, Sprite? item, Rectangle area, int max, int cur, Vector2 sp, float ss, float rot, Color sc)
{
spriteBatch = sb;
inventory = inv;
indicatorSprite = full;
emptyIndicatorSprite = empty;
itemSprite = item;
conditionIndicatorArea = area;
max_value = max;
cur_value = cur;
sprite_pos = sp;
sprite_scale = ss;
rotation = rot;
spriteColor = sc;
}
}
static private void Invoke_DrawItemStateIndicator(
SpriteBatch spriteBatch, Inventory inventory,
Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState,
bool pulsate = false)
{
AccessTools.Method(typeof(Inventory), "DrawItemStateIndicator")!
.Invoke(null, new object[] { spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, containedIndicatorArea, containedState, pulsate });
}
private static Sprite? GetTargetSprite(ConditionStorage conditionStorage, Inventory iv)
{
Inventory.ItemSlot target_slot;
{
Inventory.ItemSlot[] slots = (AccessTools.Field(typeof(Inventory), "slots").GetValue(iv)! as Inventory.ItemSlot[])!;
if (conditionStorage.slotIndex >= slots.Length)
{
DebugConsole.LogError($"ConditionStorage of {(iv.Owner as Item)!.Prefab.Identifier} specified index {conditionStorage.slotIndex} out of {slots.Length}!");
return null;
}
target_slot = slots[conditionStorage.slotIndex];
}
if (target_slot.Any())
{
Item i = target_slot.First();
return i.Prefab.InventoryIcon ?? i.Sprite;
}
else
{
return null;
}
}
public static bool Prefix(out context? __state,
SpriteBatch spriteBatch, Inventory inventory, VisualSlot slot, Item item, int slotIndex)
{
if (inventory != null && item != null && get_componentsByType(item).TryGetValue(typeof(ConditionStorage), out List<ItemComponent>? comps))
{
ConditionStorage conditionStorage = (comps.First() as ConditionStorage)!;
if (!conditionStorage.showIcon && !conditionStorage.showCount)
{
__state = null;
return true;
}
Rectangle rect = slot.Rect;
rect.Location += slot.DrawOffset.ToPoint();
if (slot.HighlightColor.A > 0)
{
float inflateAmount = (slot.HighlightColor.A / 255.0f) * slot.HighlightScaleUpAmount * 0.5f;
rect.Inflate(rect.Width * inflateAmount, rect.Height * inflateAmount);
}
var itemContainer = item.GetComponent<ItemContainer>();
Sprite? indicatorSprite;
Sprite? emptyIndicatorSprite;
Rectangle conditionIndicatorArea;
if (conditionStorage.showCount)
{
if (itemContainer is null)
{
DebugConsole.LogError($"Item {item.Prefab.Identifier} has ConditionStorage but no ItemContainer!");
__state = null;
return true;
}
if (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null)
{
__state = null;
return true;
}
int dir = slot.SubInventoryDir;
if (itemContainer.ShowContainedStateIndicator)
{
conditionIndicatorArea = new Rectangle(rect.X, rect.Bottom - (int)(10 * GUI.Scale), rect.Width, (int)(10 * GUI.Scale));
}
else
{
conditionIndicatorArea = new Rectangle(
rect.X, dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - Inventory.ContainedIndicatorHeight,
rect.Width, Inventory.ContainedIndicatorHeight);
conditionIndicatorArea.Inflate(-4, 0);
}
GUIComponentStyle indicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator.Default")!;
indicatorSprite = indicatorStyle.GetDefaultSprite();
emptyIndicatorSprite = indicatorStyle.GetSprite(GUIComponent.ComponentState.Hover);
}
else
{
indicatorSprite = null;
emptyIndicatorSprite = null;
conditionIndicatorArea = new Rectangle();
}
Vector2 itemPos;
float scale;
float rotation;
Sprite? item_sprite;
Color spriteColor;
if (conditionStorage.showIcon)
{
item_sprite = GetTargetSprite(conditionStorage, itemContainer.Inventory!);
if (item_sprite != null)
{
scale = Math.Min(Math.Min((rect.Width - 10) / item_sprite.size.X, (rect.Height - 10) / item_sprite.size.Y), 2.0f);
itemPos = rect.Center.ToVector2();
if (itemPos.Y > GameMain.GraphicsHeight)
{
itemPos.Y -= Math.Min(
(itemPos.Y + item_sprite.size.Y / 2 * scale) - GameMain.GraphicsHeight,
(itemPos.Y - item_sprite.size.Y / 2 * scale) - rect.Y);
}
rotation = 0.0f;
if (slot.HighlightColor.A > 0)
{
rotation = (float)Math.Sin(slot.HighlightTimer * MathHelper.TwoPi) * slot.HighlightTimer * 0.3f;
}
spriteColor = item_sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor();
}
else
{
scale = 1.0f;
rotation = 0.0f;
spriteColor = Color.White;
}
}
else
{
item_sprite = null;
scale = 1.0f;
rotation = 0.0f;
spriteColor = Color.White;
}
Vector2 center = rect.Center.ToVector2() + (new Vector2(conditionStorage.iconShiftX, conditionStorage.iconShiftY)) * slot.Rect.Size.ToVector2() * 0.5f;
__state = new context(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, item_sprite,
conditionIndicatorArea, conditionStorage.maxItemCount, conditionStorage.currentItemCount, center,
scale * conditionStorage.iconScale, rotation, spriteColor);
}
else
{
__state = null;
}
return true;
}
public static void Postfix(context? __state)
{
if (__state != null)
{
__state.itemSprite?.Draw(__state.spriteBatch, __state.sprite_pos, __state.spriteColor, __state.rotation, __state.sprite_scale);
if (__state.indicatorSprite != null && __state.emptyIndicatorSprite != null)
{
Invoke_DrawItemStateIndicator(__state.spriteBatch, __state.inventory, __state.indicatorSprite, __state.emptyIndicatorSprite, __state.conditionIndicatorArea,
__state.cur_value / (float)__state.max_value);
string info_text = $"{__state.cur_value}/{__state.max_value}";
float text_scale = 0.75f;
Vector2 info_size = GUIStyle.SmallFont.MeasureString(info_text) * text_scale;
GUIStyle.SmallFont.DrawString(__state.spriteBatch, info_text, __state.conditionIndicatorArea.Center.ToVector2() - (info_size * 0.5f), Color.White, 0.0f, Vector2.Zero, text_scale, SpriteEffects.None, 0.0f);
}
}
}
}
}
partial class ConditionStorage : ItemComponent, IServerSerializable
{
private CoroutineHandle? resetPredictionCoroutine = null;
private int? last_server_update_count = null;
private float resetPredictionTimer = 1.0f;
float last_update_time = 0;
const double remove_time = 1.0;
class ItemStackedInfo
{
public Item target;
public Inventory removed_from;
public Character user;
public double timestamp;
public int slot;
public ItemStackedInfo(Item item, Character character, Inventory removedFrom, int from_slot)
{
target = item;
removed_from = removedFrom;
user = character;
slot = from_slot;
timestamp = Timing.TotalTime;
}
}
// keep a list of items removed on client side so that they can be put back into container after timeour
private List<ItemStackedInfo> removed = new List<ItemStackedInfo>();
void RemoveItem_track(Item item, Character user, Inventory removedFrom, int slot)
{
removed.Add(new ItemStackedInfo(item, user, removedFrom, slot));
}
void UpdateRemovedItems()
{
var copy = removed.CreateCopy();
foreach (var item in copy)
{
// updated from server to be removed
if (item.target.Removed)
{
removed.Remove(item);
}
// timeout for removed item. put it back.
else if (Timing.TotalTime - item.timestamp > remove_time)
{
if (!item.removed_from.TryPutItem(item.target, item.slot, false, false, item.user, false, false))
{
item.target.Drop(item.user, true, true);
}
removed.Remove(item);
}
}
if (removed.Any())
{
IsActive = true;
}
}
partial void OnCountPredictionChanged()
{
if (GameMain.Client == null || !last_server_update_count.HasValue) { return; }
if (resetPredictionCoroutine == null || !CoroutineManager.IsCoroutineRunning(resetPredictionCoroutine))
{
resetPredictionCoroutine = CoroutineManager.StartCoroutine(ResetPredictionAfterDelay());
}
}
private IEnumerable<CoroutineStatus> ResetPredictionAfterDelay()
{
while (resetPredictionTimer > 0.0f)
{
resetPredictionTimer -= CoroutineManager.DeltaTime;
yield return CoroutineStatus.Running;
}
if (last_server_update_count.HasValue) { SetItemCount(last_server_update_count.Value, false); }
resetPredictionCoroutine = null;
yield return CoroutineStatus.Success;
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
if (last_update_time <= sendingTime)
{
last_update_time = sendingTime;
last_server_update_count = msg.ReadRangedInteger(0, maxItemCount);
SetItemCount(last_server_update_count.Value, true);
}
else
{
// discard the number, but still extract it from stream.
msg.ReadRangedInteger(0, maxItemCount);
}
}
}
}

View File

@@ -0,0 +1,41 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32825.248
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ItemBoxClient", "ItemBoxClient.csproj", "{D6EE7363-56EC-442E-8A50-C12111C41B59}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ItemBoxServer", "ItemBoxServer.csproj", "{35F1A00E-3387-47F2-BC89-6DB51BF829F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Debug|x64.ActiveCfg = Debug|x64
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Debug|x64.Build.0 = Debug|x64
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Release|Any CPU.Build.0 = Release|Any CPU
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Release|x64.ActiveCfg = Release|x64
{D6EE7363-56EC-442E-8A50-C12111C41B59}.Release|x64.Build.0 = Release|x64
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Debug|x64.ActiveCfg = Debug|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Debug|x64.Build.0 = Debug|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Release|Any CPU.Build.0 = Release|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Release|x64.ActiveCfg = Release|Any CPU
{35F1A00E-3387-47F2-BC89-6DB51BF829F4}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BE43C433-493F-4E78-9590-A780226B0FB3}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;CS0122</NoWarn>
<DefineConstants>$(DefineConstants)TRACE;CLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<NoWarn>1701;1702;CS0122</NoWarn>
<DefineConstants>$(DefineConstants)TRACE;CLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;CS0122</NoWarn>
<DefineConstants>$(DefineConstants)TRACE;CLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<NoWarn>1701;1702;CS0122</NoWarn>
<DefineConstants>$(DefineConstants)TRACE;CLIENT</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>..\Refs\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Barotrauma">
<HintPath>..\Refs\Client\Barotrauma.dll</HintPath>
</Reference>
<Reference Include="MonoGame.Framework.Windows.NetStandard">
<HintPath>..\Refs\MonoGame.Framework.Windows.NetStandard.dll</HintPath>
</Reference>
<Reference Include="XNATypes">
<HintPath>..\Refs\XNATypes.dll</HintPath>
</Reference>
<Compile Remove="./Server/*.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>$(DefineConstants)TRACE;SERVER</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>$(DefineConstants)TRACE;SERVER</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>..\Refs\0Harmony.dll</HintPath>
</Reference>
<Reference Include="DedicatedServer">
<HintPath>..\Refs\Server\DedicatedServer.dll</HintPath>
</Reference>
<Reference Include="MonoGame.Framework.Windows.NetStandard">
<HintPath>..\Refs\MonoGame.Framework.Windows.NetStandard.dll</HintPath>
</Reference>
<Reference Include="NetScriptAssembly">
<HintPath>..\Refs\Server\NetScriptAssembly.dll</HintPath>
</Reference>
<Reference Include="XNATypes">
<HintPath>..\Refs\XNATypes.dll</HintPath>
</Reference>
<Compile Remove="./Client/*.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RunConfig>
<Server>Standard</Server>
<Client>Standard</Client>
</RunConfig>

View File

@@ -0,0 +1,57 @@
using Barotrauma;
using HarmonyLib;
using System.Reflection;
using System.Linq;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System;
using Microsoft.Xna.Framework;
using System.ComponentModel;
using Barotrauma.Networking;
namespace BaroMod_sjx
{
partial class ConditionStorage : ItemComponent, IServerSerializable
{
/*
private CoroutineHandle? sendStateCoroutine;
private int lastSentState;
private float sendStateTimer;
*/
partial void OnCountPredictionChanged()
{
/*
sendStateTimer = 0.5f;
if (sendStateCoroutine == null)
{
sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay());
}*/
}
/*
private IEnumerable<CoroutineStatus> SendStateAfterDelay()
{
while (sendStateTimer > 0.0f)
{
sendStateTimer -= CoroutineManager.DeltaTime;
yield return CoroutineStatus.Running;
}
if (Item.Removed || GameMain.NetworkMember == null)
{
yield return CoroutineStatus.Success;
}
sendStateCoroutine = null;
if (lastSentState != currentItemCount) { Item.CreateServerEvent(this); }
yield return CoroutineStatus.Success;
}*/
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null)
{
EventData eventData = ExtractEventData<EventData>(extraData);
msg.WriteRangedInteger(eventData.ItemCount, 0, maxItemCount);
}
}
}

View File

@@ -0,0 +1,408 @@
using Barotrauma;
using HarmonyLib;
using System.Reflection;
using System.Linq;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System;
using Microsoft.Xna.Framework;
using System.ComponentModel;
using Barotrauma.Networking;
namespace BaroMod_sjx
{
partial class ItemBoxImpl : ACsMod
{
const string harmony_id = "com.sjx.ItemIOFramework";
/*
const string box_identifier = "ItemBox";
const float max_condition = 1.0f;
const int item_count = 1024;
const float increment = max_condition / item_count;
*/
private readonly Harmony harmony;
public ItemBoxImpl()
{
harmony = new Harmony(harmony_id);
harmony.PatchAll(Assembly.GetExecutingAssembly());
Barotrauma.DebugConsole.AddWarning("Loaded ItemBox Impl");
}
public override void Stop()
{
harmony.UnpatchAll(harmony_id);
}
static Dictionary<Type, List<ItemComponent>> get_componentsByType(Item item)
{
return (AccessTools.Field(typeof(Item), "componentsByType").GetValue(item)! as Dictionary<Type, List<ItemComponent>>)!;
}
[HarmonyPatch(typeof(Inventory))]
class Patch_PutItem
{
static MethodBase TargetMethod()
{
Barotrauma.DebugConsole.AddWarning("Patch_PutItem TargetMethod");
return AccessTools.Method(typeof(Inventory), "PutItem");
}
public class context
{
public Character user;
public ConditionStorage target;
public context(Character user, ConditionStorage target)
{
this.user = user;
this.target = target;
}
}
public static bool Prefix(Inventory __instance, Character user, int i, out context? __state)
{
__state = null;
ConditionStorage? target = ConditionStorage.GetFromInventory(__instance);
if (target != null && i == target.slotIndex)
{
__state = new context(user, target);
}
return true;
}
public static void Postfix(context? __state)
{
if (__state != null)
{
__state.target.OnPutItemDone(__state.user);
}
}
}
[HarmonyPatch(typeof(Inventory), nameof(Inventory.RemoveItem))]
class Patch_RemoveItem
{
public static bool Prefix(Inventory __instance, out ConditionStorage? __state, Item item)
{
__state = null;
// do not add items if sub is unloading or if removed for overflow.
if (!Submarine.Unloading)
{
ConditionStorage? target = ConditionStorage.GetFromInventory(__instance);
if (target != null)
{
if (target.GetSlot()?.Contains(item) ?? false)
{
if (target.flag_remove_no_spawn)
{
target.flag_remove_no_spawn = false;
}
else
{
target.QualityStacked = item.Quality;
target.ConditionStacked = item.Condition;
target.item_type = item.Prefab;
__state = target;
}
}
}
}
return true;
}
public static void Postfix(ConditionStorage? __state)
{
if (__state != null)
{
__state.OnRemoveItemDone();
}
}
}
[HarmonyPatch(typeof(Inventory))]
class Patch_TrySwapping
{
static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(Inventory), "TrySwapping");
}
public static bool Prefix(Inventory __instance, Item item, ref bool __result)
{
if (ConditionStorage.GetFromInventory(__instance) != null ||
(item != null && item.ParentInventory != null && ConditionStorage.GetFromInventory(item.ParentInventory) != null))
{
__result = false;
return false;
}
return true;
}
}
[HarmonyPatch(typeof(Inventory))]
class Patch_CreateNetworkEvent
{
static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(Inventory), "CreateNetworkEvent");
}
public static bool Prefix(Inventory __instance, out ConditionStorage? __state)
{
__state = null;
if (GameMain.NetworkMember != null)
{
__state = ConditionStorage.GetFromInventory(__instance);
}
return true;
}
public static void Postfix(ConditionStorage? __state)
{
if (__state != null)
{
__state.SyncItemCount();
}
}
}
}
partial class ConditionStorage : ItemComponent
{
private readonly struct EventData : IEventData
{
public readonly int ItemCount;
public EventData(int ItemCount)
{
this.ItemCount = ItemCount;
}
}
[Serialize(0, IsPropertySaveable.No, description: "Index of the stacking slot in same item's ItemContainer component")]
public int slotIndex { get; private set; }
[Serialize(true, IsPropertySaveable.No, description: "Shows count and percentage of stacking item")]
public bool showCount { get; private set; }
[Serialize(1024, IsPropertySaveable.No, description: "Maximum number of items stacked within")]
public int maxItemCount { get; private set; }
[Serialize(true, IsPropertySaveable.No, description: "Shows icon of stacking item")]
public bool showIcon { get; private set; }
[Serialize(0.6f, IsPropertySaveable.No, description: "icon scale compared to full")]
public float iconScale { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "shift x of icon")]
public float iconShiftX { get; private set; }
[Serialize(0.1f, IsPropertySaveable.No, description: "shift y of icon, down is positive")]
public float iconShiftY { get; private set; }
[Editable(minValue: 0, maxValue: int.MaxValue), Serialize(0, IsPropertySaveable.Yes, description: "Current item count")]
// camel case needed for save compatibility
public int currentItemCount
{
get => _currentItemCount;
// assume set by
set
{
SetItemCount(value, false);
}
}
void SetItemCount(int value, bool is_network_event = false)
{
if (is_network_event || GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
// authoritative number. will need to send to client later if server.
{
if (value != _currentItemCount)
{
OnCountActualChanged();
}
}
// predicted number. need to be reset later
else
{
if (value != _currentItemCount)
{
OnCountPredictionChanged();
}
}
IsActive = true;
_currentItemCount = value;
}
public ItemInventory itemInventory => Item.OwnInventory;
public ItemContainer itemContainer => Item.GetComponent<ItemContainer>();
public int _currentItemCount;
// replace setting parent container hack, so that harpoon guns work correctly
public bool flag_remove_no_spawn;
partial void OnCountActualChanged();
partial void OnCountPredictionChanged();
[Editable, Serialize("", IsPropertySaveable.Yes, description: "current stacked item")]
public Identifier ItemIdentifier
{
get
{
return item_type?.Identifier ?? "";
}
set
{
if (value.IsEmpty)
{
item_type = null;
}
else
{
item_type = ItemPrefab.Find("", value.ToIdentifier());
}
}
}
public ItemPrefab? item_type;
[Editable(MinValueInt = 0, MaxValueInt = Quality.MaxQuality), Serialize(0, IsPropertySaveable.Yes, description: "current stacked item quality")]
public int QualityStacked { get; set; }
[Editable, Serialize(float.NaN, IsPropertySaveable.Yes, description: "current stacked item condition")]
public float ConditionStacked { get; set; }
public ConditionStorage(Item item, ContentXElement element) : base(item, element) { }
public bool IsFull => currentItemCount >= maxItemCount;
public bool IsEmpty() => currentItemCount <= 0;
public void SyncItemCount()
{
#if SERVER
Item.CreateServerEvent(this, new EventData(currentItemCount));
#endif
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
SyncItemCount();
IsActive = false;
}
public static int SlotPreserveCount(ItemPrefab prefab, Inventory inventory, ItemContainer container, int slot_index)
{
int resolved_stack_size = Math.Min(Math.Min(prefab.GetMaxStackSize(inventory), container.GetMaxStackSize(slot_index)), Inventory.MaxPossibleStackSize);
if (resolved_stack_size <= 1)
{
return 1;
}
else
{
return resolved_stack_size - 1;
}
}
public static ConditionStorage? GetFromInventory(Inventory inventory)
{
if (inventory.Owner is Item parentItem)
{
return parentItem.GetComponent<ConditionStorage>();
}
else
{
return null;
}
}
public Inventory.ItemSlot? GetSlot()
{
Inventory.ItemSlot[] slots = (AccessTools.Field(typeof(Inventory), "slots").GetValue(itemInventory)! as Inventory.ItemSlot[])!;
if (slotIndex >= slots.Length)
{
DebugConsole.LogError($"ConditionStorage of {Item.Prefab.Identifier} specified index {slotIndex} out of {slots.Length}!");
return null;
}
return slots[slotIndex];
}
public void OnPutItemDone(Character user)
{
ItemContainer container = itemContainer;
Inventory.ItemSlot target_slot;
{
Inventory.ItemSlot[] slots = (AccessTools.Field(typeof(Inventory), "slots").GetValue(itemInventory)! as Inventory.ItemSlot[])!;
if (slotIndex >= slots.Length)
{
DebugConsole.LogError($"ConditionStorage of {Item.Prefab.Identifier} specified index {slotIndex} out of {slots.Length}!");
return;
}
target_slot = slots[slotIndex];
}
if (target_slot.Items.Any())
{
QualityStacked = target_slot.Items.First().Quality;
ConditionStacked = target_slot.Items.First().Condition;
item_type = target_slot.Items.First().Prefab;
if (!IsFull)
{
//bool edited = false;
int preserve = SlotPreserveCount(target_slot.Items.First().Prefab, itemInventory, container, slotIndex);
var it = target_slot.Items.ToArray().AsEnumerable().GetEnumerator();
while (it.MoveNext() && !IsFull)
{
if (preserve > 0)
{
preserve--;
}
else if (Entity.Spawner != null)
{
// client cannot despawn items, single player needs to despawn
Entity.Spawner.AddItemToRemoveQueue(it.Current);
SetItemCount(currentItemCount + 1);
flag_remove_no_spawn = true;
itemInventory.RemoveItem(it.Current);
break;
}
}
}
}
}
public void OnRemoveItemDone()
{
Inventory.ItemSlot target_slot;
{
Inventory.ItemSlot[] slots = (AccessTools.Field(typeof(Inventory), "slots").GetValue(itemInventory)! as Inventory.ItemSlot[])!;
if (slotIndex >= slots.Length)
{
DebugConsole.LogError($"ConditionStorage of {(itemInventory.Owner as Item)!.Prefab.Identifier} specified index {slotIndex} out of {slots.Length}!");
return;
}
target_slot = slots[slotIndex];
}
int preserve = SlotPreserveCount(item_type!, itemInventory, itemContainer, slotIndex);
int spawn_count = preserve - target_slot.Items.Count;
int can_spawn = Math.Min(spawn_count, currentItemCount);
// other may be queued, so spawn only one
if (can_spawn > 0)
{
if (Entity.Spawner != null)
{
SetItemCount(currentItemCount - 1);
Item.Spawner.AddItemToSpawnQueue(item_type, itemInventory,
ConditionStacked, QualityStacked, spawnIfInventoryFull: true);
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Item identifier="StackBox" category="Equipment" tags="smallitem,io_box,tool" health="1" scale="0.5" impactsoundtag="impact_metal_heavy" showcontentsintooltip="true" canflipx="false" waterproof="true" fireproof="true" Indestructible="true">
<Fabricate suitablefabricators="fabricator" outcondition="0" requiredtime="20">
<RequiredSkill identifier="mechanical" level="20" />
<RequiredItem identifier="steel" amount="2" />
</Fabricate>
<Fabricate suitablefabricators="vendingmachine" requiredtime="1" requiredmoney="10000" fabricationlimitmin="10" fabricationlimitmax="1000" quality="0" outcondition="0" />
<InventoryIcon texture="Content/Items/InventoryIconAtlas.png" sourcerect="640,256,64,64" origin="0.5,0.6" />
<Sprite texture="Content/Items/Tools/tools.png" sourcerect="314,1,94,74" origin="0.5,0.5" depth="0.6" />
<Body width="90" height="60" density="20" />
<ItemContainer capacity="1" AllowSwappingContainedItems="false" />
<ConditionStorage maxItemCount="1024" slotIndex="0" iconScale="0.6" iconShiftX="0" iconShiftY="0.1" />
<Holdable slots="Any,RightHand,LeftHand" holdpos="0,-70" handle1="-5,0" handle2="10,-20" holdangle="0" msg="ItemMsgPickUpUse" canbeselected="false" canbepicked="true" pickkey="Use" allowswappingwhenpicked="false" />
</Item>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<infotexts language="English" nowhitespace="false" translatedname="English">
<entityname.StackBox>Item Box</entityname.StackBox>
<entitydescription.StackBox>Stack your stackable items in one entity.</entitydescription.StackBox>
</infotexts>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<infotexts language="Simplified Chinese" nowhitespace="false" translatedname="中文(简体)">
<entityname.StackBox>物品箱</entityname.StackBox>
<entitydescription.StackBox>让你可叠加的物品叠加在一个实体上面</entitydescription.StackBox>
</infotexts>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="Item IO Framework" steamworkshopid="2950383008" corepackage="false" modversion="0.0.19" gameversion="1.7.7.0" installtime="1735909045" expectedhash="F6436554A34A3425D3C68FBF9DDB4FBA">
<Item file="%ModDir%/XML/ItemBox.xml" />
<Text file="%ModDir%/XML/Text/English.xml" />
<Text file="%ModDir%/XML/Text/SimplifiedChinese.xml" />
</contentpackage>

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

View File

@@ -0,0 +1,306 @@
<?xml version="1.0" encoding="utf-8"?>
<Items>
<Item name="Extinguisher Component" identifier="ExtinguisherComponent" category="Equipment" Tags="smallitem,tool,signal" cargocontaineridentifier="metalcrate" requireaimtouse="true" characterusable="false" Scale="0.65" impactsoundtag="impact_metal_light" maxstacksize="2">
<PreferredContainer primary="engcab" />
<PreferredContainer primary="wreckstoragecab,abandonedstoragecab,piratestoragecab" amount="1" spawnprobability="0.0125" />
<PreferredContainer primary="outpostcrewcabinet" amount="1" spawnprobability="0.025" />
<Deconstruct time="10">
<Item identifier="Copper" amount="2" />
</Deconstruct>
<Fabricate suitablefabricators="fabricator" requiredtime="20">
<Item identifier="Copper" amount="2" />
</Fabricate>
<Price baseprice="44">
<Price storeidentifier="merchantoutpost" minavailable="2" />
<Price storeidentifier="merchantcity" minavailable="3" />
<Price storeidentifier="merchantresearch" multiplier="1.25" minavailable="1" />
<Price storeidentifier="merchantmilitary" multiplier="1.25" minavailable="1" />
<Price storeidentifier="merchantmine" multiplier="1.25" minavailable="4" />
<Price storeidentifier="merchantengineering" minavailable="2" multiplier="0.9" />
</Price>
<Sprite texture="%ModDir%/ExtinguisherComponent.Png" sourcerect="0,0,92,52" depth="0.55" canflipx="true" origin="0.5,0.5" />
<Body width="92" height="52" density="50" />
<Holdable selectkey="Select" pickkey="Use" slots="Any,RightHand+LeftHand" msg="ItemMsgDetachWrench" PickingTime="10.0" aimpos="65,-10" handle1="0,0" attachable="true" aimable="false">
<RequiredItem identifier="wrench" type="Equipped" />
</Holdable>
<RepairTool wateramount="0.0" extinguishamount="50.0" range="600" barrelpos="0,-26" barrelrotation="270" spread="30" unskilledspread="0" targetstructures="false" hititems="false" characterusable="false" requireaimtouse="false" usablein="air">
<ParticleEmitter particle="extinguisher" velocitymin="1000.0" velocitymax="1650.0" particlespersecond="60" anglemin="-10" anglemax="10" />
<sound file="Content/Items/Tools/Extinguisher.ogg" type="OnUse" range="500.0" loop="true" />
<StatusEffect type="OnNotContained" target="This" setvalue="true" requireaimtouse="false" />
<StatusEffect type="OnContained" target="This" setvalue="true" requireaimtouse="true" />
</RepairTool>
<RepairTool wateramount="0.0" extinguishamount="50.0" range="600" barrelpos="-31,-24" barrelrotation="230" spread="30" unskilledspread="0" targetstructures="false" hititems="false" characterusable="false" requireaimtouse="false" usablein="air">
<ParticleEmitter particle="extinguisher" velocitymin="1000.0" velocitymax="1650.0" particlespersecond="60" anglemin="-10" anglemax="10" />
<sound file="Content/Items/Tools/Extinguisher.ogg" type="OnUse" range="500.0" loop="true" />
<StatusEffect type="OnNotContained" target="This" setvalue="true" requireaimtouse="false" />
<StatusEffect type="OnContained" target="This" setvalue="true" requireaimtouse="true" />
</RepairTool>
<RepairTool wateramount="0.0" extinguishamount="50.0" range="600" barrelpos="31,-24" barrelrotation="310" spread="30" unskilledspread="0" targetstructures="false" hititems="false" characterusable="false" requireaimtouse="false" usablein="air">
<ParticleEmitter particle="extinguisher" velocitymin="1000.0" velocitymax="1650.0" particlespersecond="60" anglemin="-10" anglemax="10" />
<sound file="Content/Items/Tools/Extinguisher.ogg" type="OnUse" range="500.0" loop="true" />
<StatusEffect type="OnNotContained" target="This" setvalue="true" requireaimtouse="false" />
<StatusEffect type="OnContained" target="This" setvalue="true" requireaimtouse="true" />
</RepairTool>
<LightComponent lightcolor="0,0,0,0" range="0.1" powerconsumption="1" IsOn="true" castshadows="false" allowingameediting="false">
<StatusEffect type="OnActive" target="This">
<RequiredItems items="extinguisher" type="Contained" />
<UseItem />
</StatusEffect>
<StatusEffect type="OnActive" target="Contained" condition="-6">
<RequiredItems items="extinguisher" type="Contained" />
</StatusEffect>
</LightComponent>
<ConnectionPanel selectkey="Action" canbeselected="true" msg="ItemMsgRewireScrewdriver" hudpriority="10">
<GuiFrame relativesize="0.2,0.32" minsize="400,350" maxsize="480,420" anchor="Center" style="ConnectionPanel" />
<RequiredItem items="screwdriver" type="Equipped" />
<input name="power" displayname="connection.power" />
<input name="toggle" displayname="connection.togglestate" />
<input name="set_state" displayname="connection.setstate" />
</ConnectionPanel>
<ItemContainer hideitems="false" containedspritedepth="0.56" drawinventory="true" capacity="1" AutoInteractWithContained="false" itempos="22.5,-16" itemrotation="270" canbeselected="true" msg="ItemMsgInteractSelect">
<GuiFrame relativesize="0.2,0.25" anchor="Center" minsize="140,170" maxsize="280,280" style="ItemUI" />
<Containable items="extinguisher" />
</ItemContainer>
</Item>
<Item name="Flamer Component" identifier="FlamerComponent" category="Equipment" Tags="smallitem,tool,signal" cargocontaineridentifier="metalcrate" fireproof="true" requireaimtouse="true" characterusable="false" Scale="0.45" impactsoundtag="impact_metal_light" maxstacksize="2">
<PreferredContainer primary="engcab" />
<PreferredContainer primary="wreckstoragecab,abandonedstoragecab,piratestoragecab" amount="1" spawnprobability="0.0125" />
<PreferredContainer primary="outpostcrewcabinet" amount="1" spawnprobability="0.025" />
<Deconstruct time="10">
<Item identifier="Copper" amount="2" />
</Deconstruct>
<Fabricate suitablefabricators="fabricator" requiredtime="20">
<Item identifier="Copper" amount="2" />
</Fabricate>
<Price baseprice="44">
<Price storeidentifier="merchantoutpost" minavailable="2" />
<Price storeidentifier="merchantcity" minavailable="3" />
<Price storeidentifier="merchantresearch" multiplier="1.25" minavailable="1" />
<Price storeidentifier="merchantmilitary" multiplier="1.25" minavailable="1" />
<Price storeidentifier="merchantmine" multiplier="1.25" minavailable="4" />
<Price storeidentifier="merchantengineering" minavailable="2" multiplier="0.9" />
</Price>
<Sprite texture="%ModDir%/FlamerComponent.Png" sourcerect="0,0,92,52" depth="0.55" canflipx="true" origin="0.5,0.5" />
<Body width="92" height="52" density="50" />
<Holdable selectkey="Select" pickkey="Use" slots="Any,RightHand+LeftHand" msg="ItemMsgDetachWrench" PickingTime="10.0" aimpos="65,-10" handle1="0,0" attachable="true" aimable="false">
<RequiredItem identifier="wrench" type="Equipped" />
</Holdable>
<RepairTool firedamage="25.0" structurefixamount="0.0" usablein="Air" range="600" barrelpos="0,-26" barrelrotation="270" fireprobability="0.1" repairmultiple="true" RepairMultipleWalls="false" repairthroughwalls="false" repairthroughholes="true" combatpriority="50" spread="25" unskilledspread="25">
<!-- the item must contain a welding fuel tank for it to work -->
<StatusEffect type="OnNotContained" target="This" setvalue="true" requireaimtouse="false" />
<StatusEffect type="OnContained" target="This" setvalue="true" requireaimtouse="true" />
<ParticleEmitter particle="flamethrower" particlespersecond="80" anglemin="0" anglemax="0" velocitymin="1000" velocitymax="1500" highqualitycollisiondetection="true" />
<ParticleEmitter particle="flamethrowersmoke" particlespersecond="80" anglemin="0" anglemax="0" velocitymin="500" velocitymax="1000" />
<sound file="Content/Items/Weapons/FlameThrowerLoop.ogg" type="OnUse" range="750.0" loop="true" />
<StatusEffect type="OnFailure" target="This">
<ParticleEmitter particle="bubbles" particlespersecond="20" anglemin="-10" anglemax="10" scalemin="0.3" scalemax="0.7" velocitymin="5" velocitymax="100" copyentityangle="true" />
<ParticleEmitter particle="fleshsmoke" particlespersecond="10" anglemin="-10" anglemax="10" scalemin="1" scalemax="1.5" velocitymin="5" velocitymax="200" copyentityangle="true" />
</StatusEffect>
<StatusEffect type="OnSuccess" targettype="UseTarget" targets="item" Condition="-15.0">
<Conditional HasTag="neq weldable" />
</StatusEffect>
<!-- make the item unusable when there's less than 10% oxygen -->
<StatusEffect type="OnUse" target="Hull,This" UsableIn="None">
<Conditional OxygenPercentage="lt 10" />
</StatusEffect>
<!-- make the item usable again when there's more than 10% oxygen -->
<StatusEffect type="OnUse" target="Hull,This" UsableIn="Air">
<Conditional OxygenPercentage="gt 10" />
</StatusEffect>
<!-- when using, the contained welding fuel tank will detoriate (= lose fuel) -->
<StatusEffect type="OnUse" targettype="Contained" targets="weldingfueltank" Condition="-25.0" />
<StatusEffect type="OnUse" targettype="Contained" targets="incendiumfueltank" Condition="-10.0" />
<!-- do burn damage to characters -->
<StatusEffect type="OnUse" target="UseTarget">
<Conditional InWater="false" />
<Affliction identifier="burn" amount="1.25" />
<Affliction identifier="burning" amount="2" dividebylimbcount="true" />
</StatusEffect>
<StatusEffect type="OnUse" target="UseTarget">
<RequiredItem items="incendiumfueltank" type="Contained" />
<Conditional InWater="false" />
<Affliction identifier="burn" amount="2.5" />
<Affliction identifier="burning" amount="2" dividebylimbcount="true" />
<Affliction identifier="stun" strength="1" probability="0.1" />
</StatusEffect>
<StatusEffect type="OnUse" target="This" firedamage="40.0" fireprobability="0.35" setvalue="true">
<RequiredItem items="incendiumfueltank" type="Contained" />
</StatusEffect>
<!-- explode if oxygen tanks are inserted -->
<StatusEffect type="OnUse" targettype="Contained" targets="oxygentank" delay="1.0" stackable="false" Condition="0" setvalue="true">
<sound file="Content/Items/Weapons/ExplosionSmall1.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall2.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall3.ogg" range="2000" />
<Explosion range="150.0" force="3" applyfireeffects="false">
<Affliction identifier="burn" strength="25" />
<Affliction identifier="stun" strength="5" />
</Explosion>
</StatusEffect>
<StatusEffect type="OnUse" targettype="Contained" targets="oxygenitetank" delay="1.0" stackable="false" Condition="0" setvalue="true">
<sound file="Content/Items/Weapons/ExplosionSmall1.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall2.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall3.ogg" range="2000" />
<Explosion range="150.0" force="6" applyfireeffects="false">
<Affliction identifier="burn" strength="50" />
<Affliction identifier="stun" strength="10" />
</Explosion>
</StatusEffect>
<!-- consume oxygen from the hull -->
<StatusEffect type="OnUse" target="Hull" Oxygen="-10000" />
</RepairTool>
<RepairTool firedamage="25.0" structurefixamount="0.0" usablein="Air" range="600" barrelpos="-31,-24" barrelrotation="230" fireprobability="0.1" repairmultiple="true" RepairMultipleWalls="false" repairthroughwalls="false" repairthroughholes="true" combatpriority="50" spread="25" unskilledspread="25">
<!-- the item must contain a welding fuel tank for it to work -->
<StatusEffect type="OnNotContained" target="This" setvalue="true" requireaimtouse="false" />
<StatusEffect type="OnContained" target="This" setvalue="true" requireaimtouse="true" />
<ParticleEmitter particle="flamethrower" particlespersecond="80" anglemin="0" anglemax="0" velocitymin="1000" velocitymax="1500" highqualitycollisiondetection="true" />
<ParticleEmitter particle="flamethrowersmoke" particlespersecond="80" anglemin="0" anglemax="0" velocitymin="500" velocitymax="1000" />
<sound file="Content/Items/Weapons/FlameThrowerLoop.ogg" type="OnUse" range="750.0" loop="true" />
<StatusEffect type="OnFailure" target="This">
<ParticleEmitter particle="bubbles" particlespersecond="20" anglemin="-10" anglemax="10" scalemin="0.3" scalemax="0.7" velocitymin="5" velocitymax="100" copyentityangle="true" />
<ParticleEmitter particle="fleshsmoke" particlespersecond="10" anglemin="-10" anglemax="10" scalemin="1" scalemax="1.5" velocitymin="5" velocitymax="200" copyentityangle="true" />
</StatusEffect>
<StatusEffect type="OnSuccess" targettype="UseTarget" targets="item" Condition="-15.0">
<Conditional HasTag="neq weldable" />
</StatusEffect>
<!-- make the item unusable when there's less than 10% oxygen -->
<StatusEffect type="OnUse" target="Hull,This" UsableIn="None">
<Conditional OxygenPercentage="lt 10" />
</StatusEffect>
<!-- make the item usable again when there's more than 10% oxygen -->
<StatusEffect type="OnUse" target="Hull,This" UsableIn="Air">
<Conditional OxygenPercentage="gt 10" />
</StatusEffect>
<!-- when using, the contained welding fuel tank will detoriate (= lose fuel) -->
<StatusEffect type="OnUse" targettype="Contained" targets="weldingfueltank" Condition="-25.0" />
<StatusEffect type="OnUse" targettype="Contained" targets="incendiumfueltank" Condition="-10.0" />
<!-- do burn damage to characters -->
<StatusEffect type="OnUse" target="UseTarget">
<Conditional InWater="false" />
<Affliction identifier="burn" amount="1.25" />
<Affliction identifier="burning" amount="2" dividebylimbcount="true" />
</StatusEffect>
<StatusEffect type="OnUse" target="UseTarget">
<RequiredItem items="incendiumfueltank" type="Contained" />
<Conditional InWater="false" />
<Affliction identifier="burn" amount="2.5" />
<Affliction identifier="burning" amount="2" dividebylimbcount="true" />
<Affliction identifier="stun" strength="1" probability="0.1" />
</StatusEffect>
<StatusEffect type="OnUse" target="This" firedamage="40.0" fireprobability="0.35" setvalue="true">
<RequiredItem items="incendiumfueltank" type="Contained" />
</StatusEffect>
<!-- explode if oxygen tanks are inserted -->
<StatusEffect type="OnUse" targettype="Contained" targets="oxygentank" delay="1.0" stackable="false" Condition="0" setvalue="true">
<sound file="Content/Items/Weapons/ExplosionSmall1.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall2.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall3.ogg" range="2000" />
<Explosion range="150.0" force="3" applyfireeffects="false">
<Affliction identifier="burn" strength="25" />
<Affliction identifier="stun" strength="5" />
</Explosion>
</StatusEffect>
<StatusEffect type="OnUse" targettype="Contained" targets="oxygenitetank" delay="1.0" stackable="false" Condition="0" setvalue="true">
<sound file="Content/Items/Weapons/ExplosionSmall1.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall2.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall3.ogg" range="2000" />
<Explosion range="150.0" force="6" applyfireeffects="false">
<Affliction identifier="burn" strength="50" />
<Affliction identifier="stun" strength="10" />
</Explosion>
</StatusEffect>
<!-- consume oxygen from the hull -->
<StatusEffect type="OnUse" target="Hull" Oxygen="-10000" />
</RepairTool>
<RepairTool firedamage="25.0" structurefixamount="0.0" usablein="Air" range="600" barrelpos="31,-24" barrelrotation="310" fireprobability="0.1" repairmultiple="true" RepairMultipleWalls="false" repairthroughwalls="false" repairthroughholes="true" combatpriority="50" spread="25" unskilledspread="25">
<!-- the item must contain a welding fuel tank for it to work -->
<StatusEffect type="OnNotContained" target="This" setvalue="true" requireaimtouse="false" />
<StatusEffect type="OnContained" target="This" setvalue="true" requireaimtouse="true" />
<ParticleEmitter particle="flamethrower" particlespersecond="80" anglemin="0" anglemax="0" velocitymin="1000" velocitymax="1500" highqualitycollisiondetection="true" />
<ParticleEmitter particle="flamethrowersmoke" particlespersecond="80" anglemin="0" anglemax="0" velocitymin="500" velocitymax="1000" />
<sound file="Content/Items/Weapons/FlameThrowerLoop.ogg" type="OnUse" range="750.0" loop="true" />
<StatusEffect type="OnFailure" target="This">
<ParticleEmitter particle="bubbles" particlespersecond="20" anglemin="-10" anglemax="10" scalemin="0.3" scalemax="0.7" velocitymin="5" velocitymax="100" copyentityangle="true" />
<ParticleEmitter particle="fleshsmoke" particlespersecond="10" anglemin="-10" anglemax="10" scalemin="1" scalemax="1.5" velocitymin="5" velocitymax="200" copyentityangle="true" />
</StatusEffect>
<StatusEffect type="OnSuccess" targettype="UseTarget" targets="item" Condition="-15.0">
<Conditional HasTag="neq weldable" />
</StatusEffect>
<!-- make the item unusable when there's less than 10% oxygen -->
<StatusEffect type="OnUse" target="Hull,This" UsableIn="None">
<Conditional OxygenPercentage="lt 10" />
</StatusEffect>
<!-- make the item usable again when there's more than 10% oxygen -->
<StatusEffect type="OnUse" target="Hull,This" UsableIn="Air">
<Conditional OxygenPercentage="gt 10" />
</StatusEffect>
<!-- when using, the contained welding fuel tank will detoriate (= lose fuel) -->
<StatusEffect type="OnUse" targettype="Contained" targets="weldingfueltank" Condition="-25.0" />
<StatusEffect type="OnUse" targettype="Contained" targets="incendiumfueltank" Condition="-10.0" />
<!-- do burn damage to characters -->
<StatusEffect type="OnUse" target="UseTarget">
<Conditional InWater="false" />
<Affliction identifier="burn" amount="1.25" />
<Affliction identifier="burning" amount="2" dividebylimbcount="true" />
</StatusEffect>
<StatusEffect type="OnUse" target="UseTarget">
<RequiredItem items="incendiumfueltank" type="Contained" />
<Conditional InWater="false" />
<Affliction identifier="burn" amount="2.5" />
<Affliction identifier="burning" amount="2" dividebylimbcount="true" />
<Affliction identifier="stun" strength="1" probability="0.1" />
</StatusEffect>
<StatusEffect type="OnUse" target="This" firedamage="40.0" fireprobability="0.35" setvalue="true">
<RequiredItem items="incendiumfueltank" type="Contained" />
</StatusEffect>
<!-- explode if oxygen tanks are inserted -->
<StatusEffect type="OnUse" targettype="Contained" targets="oxygentank" delay="1.0" stackable="false" Condition="0" setvalue="true">
<sound file="Content/Items/Weapons/ExplosionSmall1.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall2.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall3.ogg" range="2000" />
<Explosion range="150.0" force="3" applyfireeffects="false">
<Affliction identifier="burn" strength="25" />
<Affliction identifier="stun" strength="5" />
</Explosion>
</StatusEffect>
<StatusEffect type="OnUse" targettype="Contained" targets="oxygenitetank" delay="1.0" stackable="false" Condition="0" setvalue="true">
<sound file="Content/Items/Weapons/ExplosionSmall1.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall2.ogg" range="2000" />
<sound file="Content/Items/Weapons/ExplosionSmall3.ogg" range="2000" />
<Explosion range="150.0" force="6" applyfireeffects="false">
<Affliction identifier="burn" strength="50" />
<Affliction identifier="stun" strength="10" />
</Explosion>
</StatusEffect>
<!-- consume oxygen from the hull -->
<StatusEffect type="OnUse" target="Hull" Oxygen="-10000" />
</RepairTool>
<LightComponent lightcolor="0,0,0,0" range="0.1" powerconsumption="1" IsOn="true" castshadows="false" allowingameediting="false">
<StatusEffect type="OnActive" target="This">
<RequiredItem items="weldingfueltank" type="Contained" />
<UseItem />
</StatusEffect>
<StatusEffect type="OnActive" target="This">
<RequiredItem items="incendiumfueltank" type="Contained" />
<UseItem />
</StatusEffect>
<StatusEffect type="OnActive" target="Contained" targets="weldingfueltank" Condition="-25.0">
<RequiredItem items="weldingfueltank" type="Contained" />
</StatusEffect>
<StatusEffect type="OnActive" target="Contained" targets="incendiumfueltank" Condition="-10.0">
<RequiredItem items="incendiumfueltank" type="Contained" />
</StatusEffect>
</LightComponent>
<ConnectionPanel selectkey="Action" canbeselected="true" msg="ItemMsgRewireScrewdriver" hudpriority="10">
<GuiFrame relativesize="0.2,0.32" minsize="400,350" maxsize="480,420" anchor="Center" style="ConnectionPanel" />
<RequiredItem items="screwdriver" type="Equipped" />
<input name="power" displayname="connection.power" />
<input name="toggle" displayname="connection.togglestate" />
<input name="set_state" displayname="connection.setstate" />
</ConnectionPanel>
<ItemContainer hideitems="false" containedspritedepth="0.56" drawinventory="true" capacity="1" AutoInteractWithContained="false" itempos="22.5,-16" itemrotation="270" canbeselected="true" msg="ItemMsgInteractSelect">
<GuiFrame relativesize="0.2,0.25" anchor="Center" minsize="140,170" maxsize="280,280" style="ItemUI" />
<SlotIcon slotindex="0" texture="Content/UI/StatusMonitorUI.png" sourcerect="64,448,64,64" origin="0.5,0.5" />
<Containable items="weldingtoolfuel,oxygensource" />
</ItemContainer>
</Item>
</Items>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="ExtinguisherComponent" steamworkshopid="3389755246" corepackage="false" modversion="1.0.1" gameversion="1.7.7.0" installtime="1734967548" altnames="ExtinguisherComponent" expectedhash="A18E147B0228CB082A43503485D12761">
<Item file="%ModDir%/ExtinguisherComponent.xml" />
</contentpackage>

View File

@@ -0,0 +1,78 @@
using System.Linq;
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace BmsUtils
{
public static class Util
{
private static List<int> GetStackBoxIndex(ItemInventory inv)
{
var stackBoxsIndex = new List<int>();
for (var i = 0; i < inv.Capacity; i++)
{
var items = inv.GetItemsAt(i).ToList();
if (items.None()) { continue; }
if (items.First().Prefab.Identifier.ToString() == "StackBox" && items.First().OwnInventories.First().AllItemsMod.Any())
{
stackBoxsIndex.Add(i);
}
}
return stackBoxsIndex;
}
public static void PushItems(bool toStackBox = false)
{
var controlCharacter = Character.Controlled;
var selectedContainer = controlCharacter.SelectedItem?.GetComponent<ItemContainer>();
var leftHandItems = controlCharacter.Inventory.GetItemsAt(5).FirstOrDefault()?.OwnInventory;
var rightHandItems = controlCharacter.Inventory.GetItemsAt(6).FirstOrDefault()?.OwnInventory;
if (leftHandItems != null)
{
for (var i = 0; i < leftHandItems.Capacity; i++)
{
foreach (var item in leftHandItems.GetItemsAt(i).ToList())
{
if (toStackBox)
{
foreach (var boxIndex in GetStackBoxIndex(selectedContainer.Inventory))
{
selectedContainer.Inventory.TryPutItem(item, boxIndex, allowSwapping: false, allowCombine: true, user: null, createNetworkEvent: true);
}
}
else
{
selectedContainer.Inventory.TryPutItem(item, controlCharacter, createNetworkEvent: true, ignoreCondition: true);
}
}
}
}
if (rightHandItems != null)
{
for (var i = 0; i < rightHandItems.Capacity; i++)
{
foreach (var item in rightHandItems.GetItemsAt(i).ToList())
{
if (toStackBox)
{
foreach (var boxIndex in GetStackBoxIndex(selectedContainer.Inventory))
{
selectedContainer.Inventory.TryPutItem(item, boxIndex, allowSwapping: false, allowCombine: true, user: null, createNetworkEvent: true);
}
}
else
{
selectedContainer.Inventory.TryPutItem(item, controlCharacter, createNetworkEvent: true, ignoreCondition: true);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Reflection;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using HarmonyLib;
using System.Linq;
using Barotrauma;
using BmsUtils;
// debug
// using System.Diagnostics;
namespace Bms_Harmony
{
partial class BmsHarmony : ACsMod
{
const string harmony_id = "com.Bms.Harmony";
private readonly Harmony harmony;
public override void Stop()
{
harmony.UnpatchAll(harmony_id);
}
public BmsHarmony()
{
harmony = new Harmony(harmony_id);
harmony.PatchAll(Assembly.GetExecutingAssembly());
Barotrauma.DebugConsole.AddWarning("Loaded BmsHarmony");
}
[HarmonyPatch(typeof(Barotrauma.Items.Components.ItemContainer))]
class Patch_MergeStacks
{
static MethodBase TargetMethod()
{
Barotrauma.DebugConsole.AddWarning("Patch_MergeStacks TargetMethod");
return AccessTools.Method(typeof(Barotrauma.Items.Components.ItemContainer), "MergeStacks");
}
static bool Prefix(Barotrauma.Items.Components.ItemContainer __instance)
{
for (int i = 0; i < __instance.Inventory.Capacity - 1; i++)
{
var items = __instance.Inventory.GetItemsAt(i).ToList();
if (items.None()) { continue; }
if (items.First().Prefab.Identifier.ToString() == "StackBox" &&
items.First().OwnInventories.First().AllItemsMod.Any())
{
for (int j = 0; j < __instance.Inventory.Capacity - 1; j++)
{
var items2 = __instance.Inventory.GetItemsAt(j).ToList();
if (items2.None()) { continue; }
if (items2.First().Prefab.Identifier.ToString() != "StackBox")
{
items2.ForEach(it => __instance.Inventory.TryPutItem(it, i, allowSwapping: false, allowCombine: true, user: null, createNetworkEvent: false));
continue;
}
}
}
}
return true;
}
}
[HarmonyPatch(typeof(Barotrauma.Items.Components.ItemContainer))]
class Patch_CreateGUI
{
static MethodBase TargetMethod()
{
Barotrauma.DebugConsole.AddWarning("Patch_CreateGUI TargetMethod");
return AccessTools.Method(typeof(Barotrauma.Items.Components.ItemContainer), "CreateGUI");
}
static void Postfix(Barotrauma.Items.Components.ItemContainer __instance)
{
if (__instance.Inventory.Capacity > 1)
{
var layoutGroup = __instance.GuiFrame.FindChild(c => c is Barotrauma.GUILayoutGroup, recursive: true);
new GUIButton(new RectTransform(Vector2.One, layoutGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "PushButton")
{
ToolTip = TextManager.Get("bms.pushicon"),
OnClicked = (btn, userdata) =>
{
Util.PushItems(true);
return true;
}
};
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RunConfig>
<Server>Standard</Server>
<Client>Standard</Client>
</RunConfig>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Diemoe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<infotexts language="English" nowhitespace="false" translatedname="English">
<bms.pushicon>
All transferred to item box
</bms.pushicon>
</infotexts>

Binary file not shown.

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<infotexts language="Simplified Chinese" nowhitespace="false" translatedname="中文(简体)">
<bms.pushicon>
全部转移至物品箱
</bms.pushicon>
</infotexts>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<style>
<PushButton color="169,212,187,255" hovercolor="220,220,220,255" selectedcolor="255,255,255,255" pressedcolor="100,100,100,255" disabledcolor="125,125,125,125">
<Sprite name="PushButton" texture="%ModDir%/Text/Push.png" sourcerect="0,0,32,32" tile="false" maintainaspectratio="true" origin="0.5,0.5" />
</PushButton>
</style>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="ItemIO BetterMergeStack" steamworkshopid="3406279065" corepackage="false" modversion="1.0.2" gameversion="1.7.7.0" installtime="1737277942" expectedhash="98D1C5B0293A1539E0A330951231E576">
<Other file="%ModDir%/LICENSE" />
<UIStyle file="%ModDir%/Text/style.xml" />
<Text file="%ModDir%/Text/English.xml" />
<Text file="%ModDir%/Text/SimplifiedChinese.xml" />
</contentpackage>

View File

@@ -0,0 +1,466 @@
if SERVER then return end
LuaUserData.RegisterType("Barotrauma.Items.Components.ItemContainer+SlotRestrictions")
LuaUserData.RegisterType('System.Collections.Immutable.ImmutableArray`1[[Barotrauma.Items.Components.ItemContainer+SlotRestrictions, Barotrauma]]')
LuaUserData.MakeFieldAccessible(Descriptors['Barotrauma.Items.Components.ItemContainer'], 'slotRestrictions')
LuaUserData.MakeFieldAccessible(Descriptors['Barotrauma.ItemInventory'], 'slots')
LuaUserData.MakeFieldAccessible(Descriptors["Barotrauma.CharacterInventory"], "slots")
-- 配置重试参数
local RETRY_CONFIG = {
INTERVAL = 0.3, -- 重试间隔(秒)
MAX_ATTEMPTS = 3, -- 最大尝试次数
VALIDITY_DURATION = 5,-- 记录有效期(秒)
GENERATION_INTERVAL = 0.5 -- 分代时间间隔
}
-- 状态存储表
local retryQueue = {} -- 结构:
-- {
-- [magID] =
-- {
-- generations =
-- {
-- [generationID] = { attempts, nextAttempt, expiry },
-- ...
-- },
-- mag = entityRef
-- },
-- ...
-- }
local function isSlotFull(slotRestriction, slot)
return slotRestriction.MaxStackSize - #slot.items
end
local function tryPutItemsInInventory(character, hand, anotherhand, handInv, handIEnumerable, anotherhandIEnumerable)
-- 重试队列管理
local function addToRetryQueue(mag)
if not mag then return end
local currentTime = Timer.GetTime()
local magID = mag.ID
-- 生成分代ID每0.5秒为一个分代)
local generationID = math.floor(currentTime / RETRY_CONFIG.GENERATION_INTERVAL)
-- 初始化队列条目
if not retryQueue[magID] then
retryQueue[magID] = {
mag = mag,
generations = {}
}
end
-- 更新分代记录
local entry = retryQueue[magID]
if not entry.generations[generationID] then
entry.generations[generationID] = {
attempts = 0,
nextAttempt = currentTime + RETRY_CONFIG.INTERVAL,
expiry = currentTime + RETRY_CONFIG.VALIDITY_DURATION
}
else
-- 延长该分代的过期时间
entry.generations[generationID].expiry = currentTime + RETRY_CONFIG.VALIDITY_DURATION
end
-- print(string.format("Added generation %d for %s", generationID, mag.Name))
end
if not handInv then return end
local handInvSlots = handInv.slots
local function getPlayerInvItemsWithoutHand()
local playerInvItems = character.Inventory.AllItemsMod
-- 去除双手持有的物品,避免在双持情况下互相抢弹药
for i = #playerInvItems, 1, -1 do
local item = playerInvItems[i]
if (hand and item.ID == hand.ID) or (anotherhand and item.ID == anotherhand.ID) then
table.remove(playerInvItems, i)
end
end
return playerInvItems
end
-- 内部堆叠实现原tryStackMagzine拆分
local function tryStackMagazineInternal(mag)
if not mag or mag.ConditionPercentage > 0 then
return false
end
-- 原有堆叠逻辑
local function tryStackInInventory(inventory, Mag)
local identifier = Mag.Prefab.Identifier
for i, slot in ipairs(inventory.slots) do
for _, item in ipairs(slot.items) do
if item.HasTag("weapon") then goto continue end
if item.Prefab.Identifier.Equals(identifier) and item.ConditionPercentage == 0 and item.ID ~= Mag.ID then -- 只有空弹匣可堆叠
if inventory.CanBePutInSlot(Mag, i-1) then
inventory.TryPutItem(Mag, i-1, false, true, nil)
return true
end
end
::continue::
end
end
return false
end
-- 尝试玩家库存
if tryStackInInventory(character.Inventory, mag) then
return true
end
-- 尝试子容器
for item in getPlayerInvItemsWithoutHand() do
if item.OwnInventory and tryStackInInventory(item.OwnInventory, mag) then
return true
end
end
return false
end
-- 外部入口函数替换原tryStackMagzine
local function tryStackMagzine(mag)
if not mag then return false end
-- 立即尝试
local success = tryStackMagazineInternal(mag)
-- 失败时加入队列
if not success then
-- 防止重复添加
addToRetryQueue(mag)
end
return success
end
-- -- 堆叠弹匣
-- local function tryStackMagzine(Mag)
-- if Mag == nil or Mag.ConditionPercentage ~= 0 then return false end
-- local function tryStackInInventory(inventory, Mag)
-- local identifier = Mag.Prefab.Identifier
-- for i, slot in ipairs(inventory.slots) do
-- for _, item in ipairs(slot.items) do
-- if item.HasTag("weapon") then goto continue end
-- if item.Prefab.Identifier.Equals(identifier) and item.ConditionPercentage == 0 and item.ID ~= Mag.ID then -- 只有空弹匣可堆叠
-- if inventory.TryPutItem(Mag, i-1, false, true, nil) then
-- return true
-- end
-- end
-- ::continue::
-- end
-- end
-- return false
-- end
-- -- 尝试将弹匣堆叠到玩家物品栏1-10
-- if tryStackInInventory(character.Inventory, Mag) then
-- return true
-- end
-- -- 尝试将弹匣堆叠到玩家背包、衣服等子物品栏
-- for item in getPlayerInvItemsWithoutHand() do
-- if item.OwnInventory then
-- if tryStackInInventory(item.OwnInventory, Mag) then
-- return true
-- end
-- end
-- end
-- return false
-- end
-- 卸载弹匣
local function unloadMag(index)
local unloadedMag = handInvSlots[index].items[1]
-- 尝试堆叠弹匣
if tryStackMagzine(unloadedMag) then return true end
local slots = character.Inventory.slots
-- 如果都失败了,优先尝试将弹匣放入玩家背包、衣服子物品栏
for i = #slots, 1, -1 do
if i == 4 or i == 5 or i == 8 then
if character.Inventory.TryPutItem(unloadedMag, i-1, false, true, nil) then
return true
end
end
end
-- 然后尝试将弹匣放入玩家物品栏1-10
for i = #slots, 1, -1 do
if i <= 8 or i == 19 then goto continue end
if character.Inventory.CanBePutInSlot(unloadedMag, i-1) then
character.Inventory.TryPutItem(unloadedMag, i-1, false, false, nil)
return true
end
::continue::
end
-- 保底情况将弹匣丢到地面暂时视为false目前bool未使用
unloadedMag.Drop(character, true, true)
return false
end
-- 根据 index 构建一个含有所有可用的弹药/弹匣的 table参数 num 是要寻找的数量
local function findAvailableItemInPlayerInv(index, num)
local itemTable = {}
for item in getPlayerInvItemsWithoutHand() do
local count = 0
-- 忽略掉所有带武器标签的物品,避免从其他武器中抢弹药
if item.HasTag("weapon") then goto continue end
if handInv.CanBePutInSlot(item, index) and item.ConditionPercentage > 0 then
if itemTable[item.Prefab.Identifier.value] == nil then itemTable[item.Prefab.Identifier.value] = {} end
table.insert(itemTable[item.Prefab.Identifier.value], item)
count = count + 1
if count >= num then break end
end
if item.OwnInventory then
for item2 in item.OwnInventory.AllItemsMod do
if handInv.CanBePutInSlot(item2, index) and item2.ConditionPercentage > 0 then
if itemTable[item2.Prefab.Identifier.value] == nil then itemTable[item2.Prefab.Identifier.value] = {} end
table.insert(itemTable[item2.Prefab.Identifier.value], item2)
count = count + 1
if count >= num then break end
end
end
end
::continue::
end
local maxLength = 0
local maxElement = {}
for identifier, items in pairs(itemTable) do
if #items > maxLength then
maxLength = #items
maxElement = itemTable[identifier]
end
end
return maxElement
end
-- 根据 index 寻找可用的弹匣但不要装入unloadedMag
local function findAvailableMagInPlayerInv(index, unloadedMag)
for item in getPlayerInvItemsWithoutHand() do
-- 忽略掉所有带武器标签的物品,避免从其他武器中抢弹药
if item.HasTag("weapon") then goto continue end
if item and item.ID ~= unloadedMag.ID and handInv.CanBePutInSlot(item, index) and item.ConditionPercentage > 0 then
return item
end
if item.OwnInventory then
for item2 in item.OwnInventory.AllItemsMod do
if item2 and item2.ID ~= unloadedMag.ID and handInv.CanBePutInSlot(item2, index) and item2.ConditionPercentage > 0 then
return item2
end
end
end
::continue::
end
return nil
end
-- 根据 identifier 构建一个含有所有可用的弹药/弹匣的 table参数 num 是要寻找的数量
local function findAvailableItemWithIdentifier(identifier, num)
local findTable = {}
local count = 0
for item in getPlayerInvItemsWithoutHand() do
-- 忽略掉所有带武器标签的物品,避免从其他武器中抢弹药
if item.HasTag("weapon") then goto continue end
if item.Prefab.Identifier.Equals(identifier) then
table.insert(findTable, item)
count = count + 1
if count >= num then
return findTable
end
end
if item.OwnInventory then
for item2 in item.OwnInventory.AllItemsMod do
if item2.Prefab.Identifier.Equals(identifier) then
table.insert(findTable, item2)
count = count + 1
if count >= num then
return findTable
end
end
end
end
::continue::
end
return findTable
end
-- 根据 identifier 返回一个可用于堆叠已有弹匣的物品
local function findAvailableForStackingInPlayerInv(identifier)
local itemList = {}
for item in getPlayerInvItemsWithoutHand() do
if item.HasTag("weapon") then goto continue end
if item.Prefab.Identifier.Equals(identifier) and item.ConditionPercentage > 0 then
table.insert(itemList, item)
end
if item.OwnInventory then
for item2 in item.OwnInventory.AllItemsMod do
if item2.Prefab.Identifier.Equals(identifier) and item2.ConditionPercentage > 0 then
table.insert(itemList, item2)
end
end
end
::continue::
end
-- 对 itemList 依照 ConditionPercentage 进行升序排序
table.sort(itemList, function(a, b) return a.ConditionPercentage < b.ConditionPercentage end)
return itemList
end
local function putItem(item, index, isForStacking, isForSplitting)
if item == nil or item.ConditionPercentage == 0 or item == hand or item == anotherhand then return end
if not handInv.TryPutItem(item, index, isForStacking, isForSplitting, character, true, true)
then return false end -- 如果上弹失败则返回false
return true
end
-- 对枪械中每个 SlotRestriction 进行处理
local itemContainer = handInv.Container
local i = math.max(itemContainer.ContainedStateIndicatorSlot + 1, 1) -- 准确定位弹匣的slot
local handInvSlotRestriction = itemContainer.slotRestrictions[i-1]
-- 空物品情况
if #handInvSlots[i].items == 0 then
for _, item in ipairs(findAvailableItemInPlayerInv(i - 1, isSlotFull(handInvSlotRestriction, handInvSlots[i]))) do
putItem(item, i - 1, false, false)
end
-- 已有可堆叠弹药的情况
elseif #handInvSlots[i].items > 0 and isSlotFull(handInvSlotRestriction, handInvSlots[i]) > 0 then
for _, item in ipairs(findAvailableItemWithIdentifier(handInvSlots[i].items[1].Prefab.Identifier, isSlotFull(handInvSlotRestriction, handInvSlots[i]))) do
putItem(item, i - 1, false, false)
end
end
-- 已有弹匣的情况
if isSlotFull(handInvSlotRestriction, handInvSlots[i]) == 0 and #handInvSlots[i].items == 1 and handInvSlots[i].items[1].ConditionPercentage ~= 100 then
local itemlist = findAvailableForStackingInPlayerInv(handInvSlots[i].items[1].Prefab.Identifier)
local item = itemlist[1]
if (#itemlist == 1 and handInvSlots[i].items[1].ConditionPercentage == 0) or (item and item.ConditionPercentage ~=100 and handInvSlots[i].items[1].ConditionPercentage == 0) then --特殊情况,只剩一个弹匣下处理堆叠问题
unloadMag(i)
putItem(item, i - 1, true, true)
end
if not putItem(item, i - 1, true, true) then -- 如果上弹失败,卸载弹匣
local unloadedMag = handInvSlots[i].items[1]
unloadMag(i)
-- 如果此时双手武器未装备,重新装备武器
local currentHand = character.Inventory.GetItemInLimbSlot(handIEnumerable[1])
local currentAnotherHand = character.Inventory.GetItemInLimbSlot(anotherhandIEnumerable[1])
if (currentHand == hand and currentAnotherHand == anotherhand) ~= true then
if hand and anotherhand and hand.ID == anotherhand.ID then -- 如果为双手武器
for _, handSlotType in ipairs { InvSlotType.LeftHand, InvSlotType.RightHand } do
local handSlotIndex = character.Inventory.FindLimbSlot(handSlotType)
if handSlotIndex >= 0 then
character.Inventory.TryPutItem(hand, handSlotIndex, true, false, character, true, true)
end
end
else -- 如果为单手武器或者双持武器
character.Inventory.TryPutItem(hand, character, handIEnumerable, true, true)
character.Inventory.TryPutItem(anotherhand, character, anotherhandIEnumerable, true, true)
end
end
local findMag = findAvailableMagInPlayerInv(i - 1, unloadedMag)
if #itemlist == 0 and unloadedMag.ConditionPercentage > 0 and findMag == nil then
putItem(unloadedMag, i - 1, false, false)
else
putItem(findMag, i - 1, false, false)
end
end
tryStackMagzine(item) -- 尝试堆叠空弹匣
end
-- 注册每帧检查在多人游戏对tryStackMagazine进行重试
Hook.Add("think", "magazineRetrySystem", function()
if not retryQueue then return end
local currentTime = Timer.GetTime()
-- 遍历所有条目
for magID, entry in pairs(retryQueue) do
local mag = entry.mag
local hasValidGenerations = false
-- 实体有效性检查
if not mag or mag.ID ~= magID then
retryQueue[magID] = nil
goto continue
end
for genID, genRecord in pairs(entry.generations) do
-- 清理过期分代
if currentTime > genRecord.expiry then
entry.generations[genID] = nil
-- print("Generation expired:", genID)
goto next_generation
end
-- 执行重试条件检查
if currentTime >= genRecord.nextAttempt then
-- 执行重试
local success = tryStackMagazineInternal(mag)
if success then
-- 成功时清除全部分代
retryQueue[magID] = nil
-- print("Success via generation:", genID)
goto continue
else
-- 更新重试状态
genRecord.attempts = genRecord.attempts + 1
genRecord.nextAttempt = currentTime + RETRY_CONFIG.INTERVAL
-- 超过最大尝试次数
if genRecord.attempts >= RETRY_CONFIG.MAX_ATTEMPTS then
entry.generations[genID] = nil
-- print("Max attempts for generation:", genID)
end
end
end
hasValidGenerations = true
::next_generation::
end
-- 清理空条目
if not hasValidGenerations then
retryQueue[magID] = nil
end
::continue::
end
end)
end
Hook.Patch("Barotrauma.Character", "ControlLocalPlayer", function(instance, ptable)
if retryQueue == nil then Hook.Remove("think", "magazineRetrySystem") end
if not PlayerInput.KeyHit(Keys.R) then return end
local Character = instance
if not Character then return end
local rightHand = Character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand)
local leftHand = Character.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand)
local rightHandIEnumerable = {InvSlotType.RightHand}
local leftHandIEnumerable = {InvSlotType.LeftHand}
if not rightHand and not leftHand then return end
if rightHand and rightHand.HasTag("weapon") then
tryPutItemsInInventory(Character, rightHand, leftHand, rightHand.OwnInventory, rightHandIEnumerable, leftHandIEnumerable)
end
if leftHand and not leftHand.Equals(rightHand) and leftHand.HasTag("weapon") then
tryPutItemsInInventory(Character, leftHand, rightHand, leftHand.OwnInventory, leftHandIEnumerable, rightHandIEnumerable)
end
end, Hook.HookMethodType.After)

View File

@@ -0,0 +1,41 @@
{
"folders": [
{
"path": "."
},
],
"settings": {
"Lua.diagnostics.libraryFiles": "Enable",
"Lua.workspace.library": [
"D:/Projects/Barotrauma/types/client",
"D:/Projects/Barotrauma/types/shared",
],
"Lua.diagnostics.disable": [
"param-type-mismatch",
"return-type-mismatch",
"undefined-field",
"need-check-nil",
"assign-type-mismatch",
"redundant-return-value",
"missing-parameter",
"undefined-global",
"missing-return-value",
"undefined-doc-name",
"missing-return",
"cast-local-type",
"deprecated",
],
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "MoonSharp Attach",
"type": "moonsharp-debug",
"request": "attach",
"debugServer" : 41912
}
]
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="Press R to Reload" steamworkshopid="3413495302" corepackage="false" modversion="1.0.18" gameversion="1.7.7.0" installtime="1742817668" expectedhash="BB2C3780711FCEBBF4A73BF3AC961211" />

Binary file not shown.

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<DefaultStyles>
<CUIComponent>
<BackgroundColor>CUIPalette.Component.Background</BackgroundColor>
<Border>CUIPalette.Component.Border</Border>
<ResizeHandleColor>CUIPalette.Handle.Background</ResizeHandleColor>
<ResizeHandleGrabbedColor>CUIPalette.Handle.Grabbed</ResizeHandleGrabbedColor>
</CUIComponent>
<CUIFrame>
<BackgroundColor>CUIPalette.Frame.Background</BackgroundColor>
<OutlineColor>CUIPalette.Frame.Border</OutlineColor>
</CUIFrame>
<CUITextBlock>
<TextColor>CUIPalette.Component.Text</TextColor>
<Border>Transparent</Border>
<BackgroundColor>Transparent</BackgroundColor>
<Padding>[4,0]</Padding>
</CUITextBlock>
<CUITextInput>
<TextColor>CUIPalette.Input.Text</TextColor>
<Border>CUIPalette.Input.Border</Border>
<BackgroundColor>CUIPalette.Input.Background</BackgroundColor>
</CUITextInput>
<CUIButton>
<MasterColorOpaque>CUIPalette.Button.Background</MasterColorOpaque>
<Border>CUIPalette.Button.Border</Border>
<DisabledColor>CUIPalette.Button.Disabled</DisabledColor>
<Padding>[4,2]</Padding>
<TextAlign>[0.5,0.5]</TextAlign>
</CUIButton>
<CUICompositeButton>
<MasterColorOpaque>CUIPalette.Button.Background</MasterColorOpaque>
<DisabledColor>CUIPalette.Button.Disabled</DisabledColor>
</CUICompositeButton>
<CUIToggleButton>
<MasterColorOpaque>CUIPalette.Button.Background</MasterColorOpaque>
<Border>CUIPalette.Button.Border</Border>
<DisabledColor>CUIPalette.Button.Background</DisabledColor>
</CUIToggleButton>
<CUICloseButton>
<MasterColorOpaque>CUIPalette.CloseButton.Background</MasterColorOpaque>
<Border>Transparent</Border>
</CUICloseButton>
<DDOption>
<InactiveColor>Transparent</InactiveColor>
<Border>Transparent</Border>
<MouseOverColor>CUIPalette.DDOption.Hover</MouseOverColor>
<TextColor>CUIPalette.DDOption.Text</TextColor>
<TextAlign>[0,0]</TextAlign>
<Padding>[4,0]</Padding>
</DDOption>
<CUISlider>
<BackgroundColor>Transparent</BackgroundColor>
<Border>Transparent</Border>
</CUISlider>
<CUITickBox>
<BackgroundColor>CUIPalette.Main.Text</BackgroundColor>
</CUITickBox>
<CUICanvas>
<BackgroundColor>White</BackgroundColor>
<Border>Black</Border>
</CUICanvas>
<CUIHorizontalList>
<BackgroundColor>Transparent</BackgroundColor>
</CUIHorizontalList>
<CUIVerticalList>
<BackgroundColor>Transparent</BackgroundColor>
</CUIVerticalList>
<CUIPages>
<BackgroundColor>Transparent</BackgroundColor>
<Border>Transparent</Border>
</CUIPages>
</DefaultStyles>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<CUIFrame Absolute="[0,0,180,500]" Anchor="[0.5,0.5]">
<CUIVerticalList AKA="list" Relative="[0,0,1,1]">
<CUIHorizontalList AKA="caption" BackgroundColor="127,0,0,255" Border="127,0,0,255" Direction="Reverse" FitContent="[False,True]" Style="{ BackgroundColor : CUIPalette.Frame.Border, Border : CUIPalette.Frame.Border, TextColor : CUIPalette.Frame.Text }">
<CUICloseButton AKA="close" />
<CUITextBlock AKA="text" BackgroundColor="127,0,0,255" Border="127,0,0,255" FillEmptySpace="[True,False]" Text="caption" TextAlign="[0,0.5]" TextColor="255,229,229,255" />
</CUIHorizontalList>
<CUIHorizontalList AKA="header" BackgroundColor="76,0,0,255" Border="102,0,0,255" Direction="Reverse" FitContent="[False,True]" Style="{ BackgroundColor : CUIPalette.Header.Background, Border : CUIPalette.Header.Border, TextColor : CUIPalette.Header.Text }">
<CUITextBlock AKA="text" BackgroundColor="76,0,0,255" Border="102,0,0,255" FillEmptySpace="[True,False]" Style="{ BackgroundColor : CUIPalette.Header.Background, Border : CUIPalette.Header.Border, TextColor : CUIPalette.Header.Text }" Text="Header" TextAlign="[0,0.5]" TextColor="255,229,229,255" />
</CUIHorizontalList>
<CUIHorizontalList AKA="nav" BackgroundColor="51,0,0,255" Border="76,0,0,255" Direction="Reverse" FitContent="[False,True]" Style="{ BackgroundColor : CUIPalette.Nav.Background, Border : CUIPalette.Nav.Border, TextColor : CUIPalette.Nav.Text }">
<CUITextBlock AKA="text" BackgroundColor="51,0,0,255" Border="76,0,0,255" FillEmptySpace="[True,False]" Style="{ BackgroundColor : CUIPalette.Nav.Background, Border : CUIPalette.Nav.Border, TextColor : CUIPalette.Nav.Text }" Text="Nav" TextAlign="[0,0.5]" />
</CUIHorizontalList>
<CUIVerticalList AKA="main" BackgroundColor="25,0,0,255" Border="51,0,0,255" FillEmptySpace="[False,True]" Style="{ BackgroundColor : CUIPalette.Main.Background, Border : CUIPalette.Main.Border, TextColor : CUIPalette.Main.Text }">
<CUITextBlock AKA="text" BackgroundColor="25,0,0,255" Border="51,0,0,255" Style="{ BackgroundColor : CUIPalette.Main.Background, Border : CUIPalette.Main.Border, TextColor : CUIPalette.Main.Text }" Text="Main" TextAlign="[0,0.5]" />
<CUIButton Text="button" />
<CUIToggleButton Text="button" />
<CUITextInput Absolute="[,,,22]" />
<CUIDropDown Options="[123,321,weqwerqwe]" Selected="123" />
</CUIVerticalList>
<CUIComponent AKA="filler" Absolute="[,,,30]" />
</CUIVerticalList>
</CUIFrame>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<PaletteSet Name="Blue">
<Palette Name="blue1" BaseColor="0,0,255,255">
<Frame Background="0,0,0,200" Border="0,0,127,227" Text="229,229,255,255" />
<Header Background="0,0,76,216" Border="0,0,102,222" Text="229,229,255,255" />
<Nav Background="0,0,51,211" Border="0,0,76,216" Text="204,204,255,255" />
<Main Background="0,0,25,205" Border="0,0,51,211" Text="204,204,255,255" />
<Component Background="0,0,0,0" Border="0,0,0,0" Text="204,204,255,255" />
<Button Background="0,0,255,255" Border="0,0,127,227" Disabled="12,12,63,255" />
<CloseButton Background="51,51,255,255" />
<DDOption Background="0,0,76,216" Border="0,0,51,211" Hover="0,0,127,227" Text="204,204,255,255" />
<Handle Background="51,51,152,232" Grabbed="51,51,255,255" />
<Slider>178,178,255,255</Slider>
<Input Background="0,0,51,211" Border="0,0,76,216" Text="204,204,255,255" Focused="0,0,255,255" Invalid="255,0,0,255" Valid="0,255,0,255" Selection="178,178,255,127" Caret="178,178,255,127" />
</Palette>
<Palette Name="blue2" BaseColor="64,0,255,255">
<Frame Background="0,0,0,200" Border="32,0,127,227" Text="235,229,255,255" />
<Header Background="19,0,76,216" Border="25,0,102,222" Text="235,229,255,255" />
<Nav Background="12,0,51,211" Border="19,0,76,216" Text="216,204,255,255" />
<Main Background="6,0,25,205" Border="12,0,51,211" Text="216,204,255,255" />
<Component Background="0,0,0,0" Border="0,0,0,0" Text="216,204,255,255" />
<Button Background="64,0,255,255" Border="32,0,127,227" Disabled="25,12,63,255" />
<CloseButton Background="102,51,255,255" />
<DDOption Background="19,0,76,216" Border="12,0,51,211" Hover="32,0,127,227" Text="216,204,255,255" />
<Handle Background="76,51,152,232" Grabbed="102,51,255,255" />
<Slider>197,178,255,255</Slider>
<Input Background="12,0,51,211" Border="19,0,76,216" Text="216,204,255,255" Focused="64,0,255,255" Invalid="255,0,0,255" Valid="0,255,0,255" Selection="197,178,255,127" Caret="197,178,255,127" />
</Palette>
<Palette Name="blue3" BaseColor="0,128,255,255">
<Frame Background="0,0,0,200" Border="0,64,127,227" Text="229,242,255,255" />
<Header Background="0,38,76,216" Border="0,51,102,222" Text="229,242,255,255" />
<Nav Background="0,25,51,211" Border="0,38,76,216" Text="204,229,255,255" />
<Main Background="0,12,25,205" Border="0,25,51,211" Text="204,229,255,255" />
<Component Background="0,0,0,0" Border="0,0,0,0" Text="204,229,255,255" />
<Button Background="0,128,255,255" Border="0,64,127,227" Disabled="12,38,63,255" />
<CloseButton Background="51,153,255,255" />
<DDOption Background="0,38,76,216" Border="0,25,51,211" Hover="0,64,127,227" Text="204,229,255,255" />
<Handle Background="51,102,152,232" Grabbed="51,153,255,255" />
<Slider>178,216,255,255</Slider>
<Input Background="0,25,51,211" Border="0,38,76,216" Text="204,229,255,255" Focused="0,128,255,255" Invalid="255,0,0,255" Valid="0,255,0,255" Selection="178,216,255,127" Caret="178,216,255,127" />
</Palette>
<Palette Name="blue4" BaseColor="128,0,255,255">
<Frame Background="0,0,0,200" Border="64,0,127,227" Text="242,229,255,255" />
<Header Background="38,0,76,216" Border="51,0,102,222" Text="242,229,255,255" />
<Nav Background="25,0,51,211" Border="38,0,76,216" Text="229,204,255,255" />
<Main Background="12,0,25,205" Border="25,0,51,211" Text="229,204,255,255" />
<Component Background="0,0,0,0" Border="0,0,0,0" Text="229,204,255,255" />
<Button Background="128,0,255,255" Border="64,0,127,227" Disabled="38,12,63,255" />
<CloseButton Background="153,51,255,255" />
<DDOption Background="38,0,76,216" Border="25,0,51,211" Hover="64,0,127,227" Text="229,204,255,255" />
<Handle Background="102,51,152,232" Grabbed="153,51,255,255" />
<Slider>216,178,255,255</Slider>
<Input Background="25,0,51,211" Border="38,0,76,216" Text="229,204,255,255" Focused="128,0,255,255" Invalid="255,0,0,255" Valid="0,255,0,255" Selection="216,178,255,127" Caret="216,178,255,127" />
</Palette>
</PaletteSet>

View File

@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
namespace QICrabUI
{
/// <summary>
/// WIP, can animate any property on any object
/// Can run back and forth in [0..1] interval and
/// interpolate any property between StartValue and EndValue
/// </summary>
public class CUIAnimation
{
internal static void InitStatic()
{
CUI.OnDispose += () => ActiveAnimations.Clear();
}
public static HashSet<CUIAnimation> ActiveAnimations = new();
/// <summary>
/// This is called in CUIUpdate
/// </summary>
internal static void UpdateAllAnimations(double time)
{
foreach (CUIAnimation animation in ActiveAnimations)
{
animation.Step(time);
}
}
public bool Debug { get; set; }
public static float StartLambda = 0.0f;
public static float EndLambda = 1.0f;
private object target;
/// <summary>
/// Object containing animated property
/// </summary>
public object Target
{
get => target;
set
{
target = value;
UpdateSetter();
}
}
private bool active;
public bool Active
{
get => active;
set
{
if (Blocked || active == value) return;
active = value;
if (active) ActiveAnimations.Add(this);
else ActiveAnimations.Remove(this);
ApplyValue();
}
}
/// <summary>
/// In seconds
/// </summary>
public double Duration
{
get => 1.0 / Speed * Timing.Step;
set
{
double steps = value / Timing.Step;
Speed = 1.0 / steps;
}
}
public double ReverseDuration
{
get => 1.0 / (BackSpeed ?? 0) * Timing.Step;
set
{
double steps = value / Timing.Step;
BackSpeed = 1.0 / steps;
}
}
/// <summary>
/// Will prevent it from starting
/// </summary>
public bool Blocked { get; set; }
/// <summary>
/// Progress of animation [0..1]
/// </summary>
public double Lambda { get; set; }
/// <summary>
/// Lambda increase per update step, calculated when you set Duration
/// </summary>
public double Speed { get; set; } = 0.01;
public double? BackSpeed { get; set; }
/// <summary>
/// If true animation won't stop when reaching end, it will change direction
/// </summary>
public bool Bounce { get; set; }
/// <summary>
/// Straight, Reverse
/// </summary>
public CUIDirection Direction { get; set; }
/// <summary>
/// Value will be interpolated between these values
/// </summary>
public object StartValue { get; set; }
public object EndValue { get; set; }
private string property;
private Action<object> setter;
private Type propertyType;
/// <summary>
/// Property name that is animated
/// </summary>
public string Property
{
get => property;
set
{
property = value;
UpdateSetter();
}
}
public event Action<CUIDirection> OnStop;
/// <summary>
/// You can set custon Interpolate function
/// </summary>
public Func<float, object> Interpolate
{
get => interpolate;
set
{
customInterpolate = value;
UpdateSetter();
}
}
private Func<float, object> customInterpolate;
private Func<float, object> interpolate;
//...
public Action<object> Convert<T>(Action<T> myActionT)
{
if (myActionT == null) return null;
else return new Action<object>(o => myActionT((T)o));
}
private void UpdateSetter()
{
if (Target != null && Property != null)
{
PropertyInfo pi = Target.GetType().GetProperty(Property);
if (pi == null)
{
CUI.Warning($"CUIAnimation couldn't find {Property} in {Target}");
return;
}
propertyType = pi.PropertyType;
interpolate = customInterpolate ?? ((l) => CUIInterpolate.Interpolate[propertyType].Invoke(StartValue, EndValue, l));
// https://coub.com/view/1mast0
if (propertyType == typeof(float))
{
setter = Convert<float>(pi.GetSetMethod()?.CreateDelegate<Action<float>>(Target));
}
if (propertyType == typeof(Color))
{
setter = Convert<Color>(pi.GetSetMethod()?.CreateDelegate<Action<Color>>(Target));
}
}
}
/// <summary>
/// Resumes animation in the same direction
/// </summary>
public void Start() => Active = true;
public void Stop()
{
Active = false;
OnStop?.Invoke(Direction);
}
/// <summary>
/// Set Direction to Straight and Start
/// </summary>
public void Forward()
{
Direction = CUIDirection.Straight;
Active = true;
}
/// <summary>
/// Set Direction to Reverse and Start
/// </summary>
public void Back()
{
Direction = CUIDirection.Reverse;
Active = true;
}
/// <summary>
/// Set Lambda to 0
/// </summary>
public void SetToStart() => Lambda = StartLambda;
/// <summary>
/// Set Lambda to 1
/// </summary>
public void SetToEnd() => Lambda = EndLambda;
private void UpdateState()
{
if (Direction == CUIDirection.Straight && Lambda >= EndLambda)
{
Lambda = EndLambda;
if (Bounce) Direction = CUIDirection.Reverse;
else Stop();
}
if (Direction == CUIDirection.Reverse && Lambda <= StartLambda)
{
Lambda = StartLambda;
if (Bounce) Direction = CUIDirection.Straight;
else Stop();
}
}
public void ApplyValue()
{
if (interpolate == null) return;
object value = interpolate.Invoke((float)Lambda);
setter?.Invoke(value);
}
public void Step(double time)
{
UpdateState();
ApplyValue();
Lambda += Direction == CUIDirection.Straight ? Speed : -(BackSpeed ?? Speed);
if (Debug) LogStatus();
}
public void LogStatus() => CUI.Log($"Active:{Active} Direction:{Direction} Lambda:{Lambda}");
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
namespace QICrabUI
{
/// <summary>
/// Class containing a few interpolate functions for CUIAnimation
/// </summary>
public class CUIInterpolate
{
public static object InterpolateColor(object start, object end, double lambda)
{
return ((Color)start).To(((Color)end), (float)lambda);
}
public static object InterpolateVector2(object start, object end, double lambda)
{
Vector2 a = (Vector2)start;
Vector2 b = (Vector2)end;
return a + (b - a) * (float)lambda;
}
public static object InterpolateFloat(object start, object end, double lambda)
{
float a = (float)start;
float b = (float)end;
return a + (b - a) * (float)lambda;
}
public static Dictionary<Type, Func<object, object, double, object>> Interpolate = new();
internal static void InitStatic()
{
CUI.OnInit += () =>
{
Interpolate[typeof(Color)] = InterpolateColor;
Interpolate[typeof(Vector2)] = InterpolateVector2;
Interpolate[typeof(float)] = InterpolateFloat;
};
CUI.OnDispose += () =>
{
Interpolate.Clear();
};
}
}
}

View File

@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using HarmonyLib;
using System.Runtime.CompilerServices;
[assembly: IgnoresAccessChecksTo("Barotrauma")]
[assembly: IgnoresAccessChecksTo("DedicatedServer")]
[assembly: IgnoresAccessChecksTo("BarotraumaCore")]
namespace QICrabUI
{
/// <summary>
/// In fact a static class managing static things
/// </summary>
public partial class CUI
{
public static Vector2 GameScreenSize => new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
public static Rectangle GameScreenRect => new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
private static string modDir;
public static string ModDir
{
get => modDir;
set
{
modDir = value;
LuaFolder = Path.Combine(value, @"Lua");
}
}
public static bool UseLua { get; set; } = true;
public static string LuaFolder { get; set; }
private static string assetsPath;
public static string AssetsPath
{
get => assetsPath;
set
{
assetsPath = value;
PalettesPath = Path.Combine(value, @"Palettes");
}
}
public static string CUITexturePath = "CUI.png";
public static string PalettesPath { get; set; }
/// <summary>
/// If set CUI will also check this folder when loading textures
/// </summary>
public static string PGNAssets
{
get => TextureManager.PGNAssets;
set => TextureManager.PGNAssets = value;
}
private static List<CUI> Instances = new List<CUI>();
/// <summary>
/// The singleton
/// </summary>
public static CUI Instance
{
get
{
if (Instances.Count == 0) return null;
return Instances.First();
}
set
{
Instances.Clear();
if (value != null) Instances.Add(value);
}
}
/// <summary>
/// Orchestrates Drawing and updates, there could be only one
/// CUI.Main is located under vanilla GUI
/// </summary>
public static CUIMainComponent Main => Instance?.main;
/// <summary>
/// Orchestrates Drawing and updates, there could be only one
/// CUI.TopMain is located above vanilla GUI
/// </summary>
public static CUIMainComponent TopMain => Instance?.topMain;
/// <summary>
/// Snapshot of mouse and keyboard state
/// </summary>
public static CUIInput Input => Instance?.input;
/// <summary>
/// Safe texture manager
/// </summary
public static CUITextureManager TextureManager => Instance?.textureManager;
/// <summary>
/// Adapter to vanilla focus system, don't use
/// </summary>
public static CUIFocusResolver FocusResolver => Instance?.focusResolver;
public static CUILuaRegistrar LuaRegistrar => Instance?.luaRegistrar;
public static CUIComponent FocusedComponent
{
get => FocusResolver.FocusedCUIComponent;
set => FocusResolver.FocusedCUIComponent = value;
}
/// <summary>
/// This affects logging
/// </summary>
public static bool Debug;
/// <summary>
/// Will break the mod if it's compiled
/// </summary>
public static bool UseCursedPatches { get; set; } = false;
/// <summary>
/// It's important to set it, if 2 CUIs try to add a hook with same id one won't be added
/// </summary>
public static string HookIdentifier
{
get => hookIdentifier;
set
{
hookIdentifier = value?.Replace(' ', '_');
}
}
private static string hookIdentifier = "";
public static string CUIHookID => $"QICrabUI.{HookIdentifier}";
public static Harmony harmony;
public static Random Random = new Random();
/// <summary>
/// Called on first Initialize
/// </summary>
public static event Action OnInit;
/// <summary>
/// Called on last Dispose
/// </summary>
public static event Action OnDispose;
public static bool Disposed { get; set; } = true;
public static event Action<TextInputEventArgs> OnWindowTextInput;
public static event Action<TextInputEventArgs> OnWindowKeyDown;
//public static event Action<TextInputEventArgs> OnWindowKeyUp;
//TODO this doesn't trigger when you press menu button, i need to go inside thet method
public static event Action OnPauseMenuToggled;
public static void InvokeOnPauseMenuToggled() => OnPauseMenuToggled?.Invoke();
public static bool InputBlockingMenuOpen
{
get
{
if (IsBlockingPredicates == null) return false;
return IsBlockingPredicates.Any(p => p());
}
}
public static List<Func<bool>> IsBlockingPredicates => Instance?.isBlockingPredicates;
private List<Func<bool>> isBlockingPredicates = new List<Func<bool>>();
/// <summary>
/// In theory multiple mods could use same CUI instance,
/// i clean it up when UserCount drops to 0
/// </summary>
public static int UserCount = 0;
/// <summary>
/// An object that contains current mouse and keyboard states
/// It scans states at the start on Main.Update
/// </summary>
private CUIInput input = new CUIInput();
private CUIMainComponent main;
private CUIMainComponent topMain;
private CUITextureManager textureManager = new CUITextureManager();
private CUIFocusResolver focusResolver = new CUIFocusResolver();
private CUILuaRegistrar luaRegistrar = new CUILuaRegistrar();
public static void ReEmitWindowTextInput(object sender, TextInputEventArgs e) => OnWindowTextInput?.Invoke(e);
public static void ReEmitWindowKeyDown(object sender, TextInputEventArgs e) => OnWindowKeyDown?.Invoke(e);
//public static void ReEmitWindowKeyUp(object sender, TextInputEventArgs e) => OnWindowKeyUp?.Invoke(e);
private void CreateMains()
{
main = new CUIMainComponent() { AKA = "Main Component" };
topMain = new CUIMainComponent() { AKA = "Top Main Component" };
}
/// <summary>
/// Should be called in IAssemblyPlugin.Initialize
/// \todo make it CUI instance member when plugin system settle
/// </summary>
public static void Initialize()
{
CUIDebug.Log($"CUI.Initialize {HookIdentifier} Instance:[{Instance?.GetHashCode()}] UserCount:{UserCount}", Color.Lime);
if (Instance == null)
{
Disposed = false;
Instance = new CUI();
Stopwatch sw = Stopwatch.StartNew();
if (HookIdentifier == null || HookIdentifier == "") CUI.Warning($"Warning: CUI.HookIdentifier is not set, this mod may conflict with other mods that use CUI");
InitStatic();
// this should init only static stuff that doesn't depend on instance
OnInit?.Invoke();
Instance.CreateMains();
GameMain.Instance.Window.TextInput += ReEmitWindowTextInput;
GameMain.Instance.Window.KeyDown += ReEmitWindowKeyDown;
//GameMain.Instance.Window.KeyUp += ReEmitWindowKeyUp;
CUIDebug.Log($"CUI.OnInit?.Invoke took {sw.ElapsedMilliseconds}ms");
sw.Restart();
harmony = new Harmony(CUIHookID);
PatchAll();
CUIDebug.Log($"CUI.PatchAll took {sw.ElapsedMilliseconds}ms");
AddCommands();
sw.Restart();
LuaRegistrar.Register();
CUIDebug.Log($"CUI.LuaRegistrar.Register took {sw.ElapsedMilliseconds}ms");
}
UserCount++;
CUIDebug.Log($"CUI.Initialized {HookIdentifier} Instance:[{Instance?.GetHashCode()}] UserCount:{UserCount}", Color.Lime);
}
public static void OnLoadCompleted()
{
//Idk doesn't work
//CUIMultiModResolver.FindOtherInputs();
}
/// <summary>
/// Should be called in IAssemblyPlugin.Dispose
/// </summary>
public static void Dispose()
{
CUIDebug.Log($"CUI.Dispose {HookIdentifier} Instance:[{Instance?.GetHashCode()}] UserCount:{UserCount}", Color.Lime);
UserCount--;
if (UserCount <= 0)
{
RemoveCommands();
// harmony.UnpatchAll(harmony.Id);
harmony.UnpatchAll();
TextureManager.Dispose();
CUIDebugEventComponent.CapturedIDs.Clear();
OnDispose?.Invoke();
Disposed = true;
Instance.isBlockingPredicates.Clear();
Errors.Clear();
LuaRegistrar.Deregister();
Instance = null;
UserCount = 0;
CUIDebug.Log($"CUI.Disposed {HookIdentifier} Instance:[{Instance?.GetHashCode()}] UserCount:{UserCount}", Color.Lime);
}
GameMain.Instance.Window.TextInput -= ReEmitWindowTextInput;
GameMain.Instance.Window.KeyDown -= ReEmitWindowKeyDown;
//GameMain.Instance.Window.KeyUp -= ReEmitWindowKeyUp;
}
//HACK Why it's set to run in static constructor?
// it runs perfectly fine in CUI.Initialize
//TODO component inits doesn't depend on the order
// why am i responsible for initing them here?
internal static void InitStatic()
{
CUIExtensions.InitStatic();
CUIInterpolate.InitStatic();
CUIAnimation.InitStatic();
CUIReflection.InitStatic();
CUIMultiModResolver.InitStatic();
CUIPalette.InitStatic();
CUIMap.CUIMapLink.InitStatic();
CUIMenu.InitStatic();
CUIComponent.InitStatic();
CUITypeMetaData.InitStatic();
CUIStyleLoader.InitStatic();
}
}
}

View File

@@ -0,0 +1,208 @@
## 0.2.5.1
Experimenting with the way multiple CUIs resolve conflicts
Renamed CUI.UpdateHookIdentifier => CUI.HookIdentifier
now i'm using it in harmony patches to
added warning if it's not set
fixed crash in GUI_UpdateMouseOn_Postfix
Added null checks in GUI_UpdateMouseOn_Postfix
## 0.2.5.0
Added CUI.UpdateHookIdentifier it will be set as identifier to CUI think hook, it very important to set it or hooks from different CUIs will conflict
Added CUIAnimation
Added IgnoreTransparent prop, if true mouse events will work only on not transparent sprites
Added Transparency prop, it multiplies BackgroundColor.A and is propagated to Children
Made CUISpite an object... again
Added Rotation, Origin and Offset to CUISprite
Added option to load CUISprites with base folder, which allows deserialized components to load sprites from the same folder with relative paths
Added CUIMenu, check "Custom Menus" mod, CUIRadialMenu (the ugly brother of CUIMenu)
Added more docs
## 0.2.4.0
"Fixed" cursed bug that made MainComponents become in GameMain.Update patch after multiple lobbies in compiled version
But this "fix" seems to decrease update smoothness, so i might rethink later
Set CUI.UseCursedPatches to true if you're not affraid
Added more performance measurements, shortcutted dumb class scanning in CUILuaRegistrar that happened even if you didn't use lua
Buttons now update their color only on events and not in draw cycle, added AutoUpdateColor to prevent this color change in case you want to control it manually (why?)
Added confusing event InvokeOnMouseOff which is symmetrical to InvokeOnMouseOn but happens on previous MouseOn list, and it turned out to be essential to e.g. switch color when mouse leaves a button
You can now limit resize directions with CUIComponent.ResizeDirection
Fixed forsed size not reseting after removing a textblock
Added cuiprinttree command along with cuidraworder
## 0.2.3.0
Made CUITextInput, CUITickBox and CUISlider use commands and consume data
Added Gap to CUIVerticalList
Made OnAnyCommand,OnAnyData,OnConsume events instead of delegates
added ReflectCommands and RetranslateCommands props, so you could define in xml that a wrapper should sync state between its children
Setting a Palette prop now won't automatically set palette for all children because it doesn't work as intended on deserialized components, use DeepPalette instead
CUISlider now have Range and Precision
CUI.OnPauseMenuToggled will now trigger even when resume button in pause menu is pressed
You can no just set CUIPalette.DefaultPalette before CUI.Initialize instead of manually loading it
Palettes now remember their BaseColor so you could replicate them
Added more useless CUIPrefabs, i think they are too specific and need some redesign, perhaps i need to create some builder
Added FloatRange alongside with IntRange
fixed crash in KeyboardDispatcher_set_Subscriber_Replace in compiled mods
## 0.2.2.1
Minor stuff: multibutton dispatches the command, CUIPages resizes page to [0,0,1,1], maincomponent flatten tree is a bit more thread safe
Added IRefreshable interface and CUIComponent.CascadeRefresh
CascadeRefresh will recursivelly call Refresh on every child that implements IRefreshable
## 0.2.2.0
Added to CUI.cs
```
using System.Runtime.CompilerServices;
[assembly: IgnoresAccessChecksTo("Barotrauma")]
[assembly: IgnoresAccessChecksTo("DedicatedServer")]
[assembly: IgnoresAccessChecksTo("BarotraumaCore")]
```
So it could be compiled
#### Temporary solution to pathing:
Now mod won't automatially find its folders
If you want to use lua you need to set CUI.ModDir to the mod folder path
Also you need to place Assets folder with CUI stuff somewhere in your mod and set CUI.AssetsPath to it
You can rename it, just set the path
All this needs to be done before CUI.Initialize()
## 0.2.1.0
Dried tree building methods, added tests for them
Added insert method along with append and prepend, unlike List.Insert it won't throw on "index out of bounds"
## 0.2.0.1
.nojekyll moment
## 0.2.0.0
Reworked CUIPalette, and CUICommand, check docs
Reworked border, added separate borders for each side, border sprite, outline
Changed how zindex is calculated, now every next child will have higher zindex -> everything in one frame will be above or below everything in the other
optimized CUITextBlock measurements, added some validation to CUITextInput
Added CUIPresets with... presets. Which you can use to reduce boilerplate code
Made more stuff parsable and serializble
And tons of other things i'm too lazy to write down, compare commits if you're curious
## 0.1.0.0
You can now use relative paths for sprite textures
You can set CUI.PGNAssets to the folder with your assets, CUI will also search for textures there
Reworked MasterColorOpaque, it now just uses base color alpha
Synced vertical and horizontal lists, added ScrollSpeed
OnUpdate event now invoked before layout calcs, Also added OnLayoutUpdated event, it's invoked before recalc of children layouts
"Fixed" imprecise rects that e.g. caused sprite overlap and gaps
Added CrossRelative prop, it's like Relative but values are relative to the oposite value, e.g. CrossRelative.Width = Host.Real.Height, convinient for making square things
Reworked slider component
DragHandle can now DragRelative to the parent
#### Serialization
Added BreakSerialization prop, if true serrialization will stop on this component
Added LoadSelfFromFile, SaveToTheSamePath methods
Added Hydrate method, it will run right after deserizlization, allowing you to access children with Get<> and e.g. save them to local vars
Added SaveAfterLoad prop, it's mostly for serialization debugging
Added more Custom ToString and parsed methods to CUIExtensions, Added native enum serialization, Vector2 and Rectangle is now serialized into [ x, y ], be carefull
Sprite is now fully serializable
## 0.0.5.1
Added "[CUISerializable] public string Command { get; set; }"" to CUIButton so you could define command that is called on click in xml
Splitted MasterColor into MasterColor and MasterColorOpaque
CUITextBlock RealTextSize is now rounded because 2.199999 is prone to floating-point errors
Added MasterColor to CUIToggleButton
Buttons now will folow a pattern: if state is changed by user input then it'll invoke StateChanged event
If state is changed from code then it won't invoke the event
When you change state from code you already know about that so you don't need an event
And on the other side if event is always fired it will create un-untangleable loops when you try to sync state between two buttons
Fixed CUIComponent.Get< T > method, i forgot to add it to docs, it gives you memorized component by its name, so it's same as frame["header"], but it can also give you nested components like that `Header = Frame.Get<CUIHorizontalList>("layout.content.header");`
Exposed ResizeHandles, you can hide them separately
Fixed crash when serializing a Vector2, added more try-catches and warnings there
Fixed Sprites in CUI.png being 33x33, i just created a wrong first rectangle and then copy-pasted it, guh
Added sprites to resize handles, and gradient sprite that's not used yet
Added `public SpriteEffects Effects;` to CUISprite, it controls texture flipping
More params in CUITextureManager.GetSprite
## 0.0.5.0
Added Styles
Every component has a Style and every Type has a DefaultStyle
Styles are observable dicts with pairs { "prop name", "prop value" } and can be used to assign any parsable string to any prop or reference some value from CUIPalette
## 0.0.4.0
Added CUICanvas.Render(Action< SpriteBatch > renderFunc) method that allows you to render anything you want onto the canvas texture
## 0.0.3.0
Added Changelog.md :BaroDev:
Added CUI.TopMain, it's the second CUIMainComponent which exists above vanilla GUI
Added printsprites command, it prints styles from GUIStyle.ComponentStyles
More fabric methods for CUISprite

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace QICrabUI
{
/// <summary>
/// A button
/// It's derived from CUITextBlock and has all its props
/// </summary>
public class CUIButton : CUITextBlock
{
[CUISerializable]
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
[CUISerializable] public Color DisabledColor { get; set; }
[CUISerializable] public Color InactiveColor { get; set; }
[CUISerializable] public Color MouseOverColor { get; set; }
[CUISerializable] public Color MousePressedColor { get; set; }
[CUISerializable] public bool AutoUpdateColor { get; set; } = true;
/// <summary>
/// Convenient prop to set all colors at once
/// </summary>
public Color MasterColor
{
set
{
InactiveColor = value.Multiply(0.7f);
MouseOverColor = value.Multiply(0.9f);
MousePressedColor = value;
DetermineColor();
}
}
public Color MasterColorOpaque
{
set
{
InactiveColor = new Color((int)(value.R * 0.7f), (int)(value.G * 0.7f), (int)(value.B * 0.7f), value.A);
MouseOverColor = new Color((int)(value.R * 0.9f), (int)(value.G * 0.9f), (int)(value.B * 0.9f), value.A);
MousePressedColor = value;
DetermineColor();
}
}
/// <summary>
/// BackgroundColor is used in base.Draw, but here it's calculated from colors above
/// So it's not a prop anymore, and i don't want to serialize it
/// </summary>
public new Color BackgroundColor
{
get => CUIProps.BackgroundColor.Value;
set => CUIProps.BackgroundColor.SetValue(value);
}
public void DetermineColor()
{
if (!AutoUpdateColor) return;
if (Disabled)
{
BackgroundColor = DisabledColor;
}
else
{
BackgroundColor = InactiveColor;
if (MouseOver) BackgroundColor = MouseOverColor;
if (MousePressed) BackgroundColor = MousePressedColor;
}
}
public override void Draw(SpriteBatch spriteBatch)
{
//DetermineColor();
base.Draw(spriteBatch);
}
public CUIButton() : base()
{
Text = "CUIButton";
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
OnMouseDown += (e) =>
{
if (!Disabled)
{
SoundPlayer.PlayUISound(ClickSound);
if (Command != null && Command != "")
{
DispatchUp(new CUICommand(Command));
}
}
};
OnMouseOff += (e) => DetermineColor();
OnMouseOn += (e) => DetermineColor();
OnStyleApplied += DetermineColor;
DetermineColor();
}
public CUIButton(string text) : this()
{
Text = text;
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Allows you to manipulate pixel data of its texture
/// </summary>
public class CUICanvas : CUIComponent, IDisposable
{
public Color[] Data;
public RenderTarget2D Texture;
/// <summary>
/// Size of the internal texture
/// Will automatically resize the texture and data array of set
/// </summary>
public virtual Point Size
{
get => new Point(Texture.Width, Texture.Height);
set
{
if (value.X == Texture?.Width && value.Y == Texture?.Height) return;
RenderTarget2D oldTexture = Texture;
Texture = new RenderTarget2D(GameMain.Instance.GraphicsDevice, value.X, value.Y);
Data = new Color[Texture.Width * Texture.Height];
BackgroundSprite = new CUISprite(Texture);
oldTexture?.Dispose();
}
}
public void Clear(Color? color = null)
{
Color cl = color ?? Color.Transparent;
for (int i = 0; i < Data.Length; i++)
{
Data[i] = cl;
}
SetData();
}
public Color GetPixel(int x, int y)
{
return Data[y * Texture.Width + x];
}
public void SetPixel(int x, int y, Color cl)
{
Data[y * Texture.Width + x] = cl;
}
/// <summary>
/// Call this method to transfer values from Data array into texture
/// </summary>
public void SetData()
{
Texture.SetData<Color>(Data);
}
/// <summary>
/// Uses renderFunc to render stuff directy onto Canvas.Texture
/// You can for example use GUI "Draw" methods with provided spriteBatch
/// </summary>
/// <param name="renderFunc"> Action<SpriteBatch> where you can draw whatever you want </param>
public void Render(Action<SpriteBatch> renderFunc)
{
GameMain.Instance.GraphicsDevice.SetRenderTarget(Texture);
//TODO save and restore scissor rect
spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable);
renderFunc(spriteBatch);
spriteBatch.End();
GameMain.Instance.GraphicsDevice.SetRenderTarget(null);
}
public SpriteBatch spriteBatch;
public CUICanvas(int x, int y) : base()
{
Size = new Point(x, y);
spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice);
}
public CUICanvas() : this(100, 100) { }
public override void CleanUp()
{
Texture?.Dispose();
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent : IDisposable
{
private void SetupAnimations()
{
Animations = new Indexer<string, CUIAnimation>(
(key) => animations.GetValueOrDefault(key),
(key, value) => AddAnimation(key, value)
);
}
private Dictionary<string, CUIAnimation> animations = new();
public Indexer<string, CUIAnimation> Animations;
public void AddAnimation(string name, CUIAnimation animation)
{
animation.Target = this;
animations[name] = animation;
}
public void BlockChildrenAnimations()
{
foreach (CUIComponent child in Children)
{
foreach (CUIAnimation animation in child.animations.Values)
{
animation.Stop();
animation.Blocked = true;
}
child.BlockChildrenAnimations();
}
}
}
}

View File

@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent
{
/// <summary>
/// Just a wrapper for CUIProps
/// idk how to separate them better
/// </summary>
//TODO this should be a dict, and cuiprop should have hash
public CUIComponentProps CUIProps { get; set; } = new();
public class CUIComponentProps
{
public CUIProp<int?> ZIndex = new CUIProp<int?>()
{
LayoutProp = true,
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
//HACK think, should i propagate null?
if (v.HasValue && !child.IgnoreParentZIndex)
{
child.ZIndex = v.Value + 1;
}
}
},
};
public CUIProp<bool> IgnoreEvents = new CUIProp<bool>()
{
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
if (!child.IgnoreParentEventIgnorance) child.IgnoreEvents = v;
}
},
};
public CUIProp<bool> Visible = new CUIProp<bool>()
{
Value = true,
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
if (!child.IgnoreParentVisibility) child.Visible = v;
}
},
};
public CUIProp<bool> Revealed = new CUIProp<bool>()
{
Value = true,
OnSet = (v, host) =>
{
// host.TreeChanged = true;
host.Visible = v;
host.IgnoreEvents = !v;
},
};
public CUIProp<CUIBool2> Ghost = new CUIProp<CUIBool2>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<bool> CullChildren = new CUIProp<bool>()
{
OnSet = (v, host) =>
{
host.HideChildrenOutsideFrame = v;
},
};
public CUIProp<CUI3DOffset> ChildrenOffset = new CUIProp<CUI3DOffset>()
{
ChildProp = true,
Value = new CUI3DOffset(0, 0, 1), // uuuuuuuuu suka blyat!
Validate = (v, host) => host.ChildOffsetBounds.Check(v),
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
if (!child.Fixed) child.Scale = v.Z;
}
},
};
public CUIProp<bool> ResizeToSprite = new CUIProp<bool>()
{
LayoutProp = true,
OnSet = (v, host) =>
{
if (v)
{
host.Absolute = host.Absolute with
{
Width = host.BackgroundSprite.SourceRect.Width,
Height = host.BackgroundSprite.SourceRect.Height,
};
}
},
};
public CUIProp<CUIBool2> FillEmptySpace = new CUIProp<CUIBool2>()
{
LayoutProp = true,
};
public CUIProp<CUIBool2> FitContent = new CUIProp<CUIBool2>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> Absolute = new CUIProp<CUINullRect>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> AbsoluteMin = new CUIProp<CUINullRect>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> AbsoluteMax = new CUIProp<CUINullRect>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> Relative = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
public CUIProp<CUINullRect> RelativeMin = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
public CUIProp<CUINullRect> RelativeMax = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
public CUIProp<CUINullRect> CrossRelative = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
#region Graphic Props --------------------------------------------------------
#endregion
public CUIProp<PaletteOrder> Palette = new CUIProp<PaletteOrder>()
{
ShowInDebug = false,
OnSet = (v, host) =>
{
//TODO should this be called in deserialize?
CUIGlobalStyleResolver.OnComponentStyleChanged(host);
// foreach (var child in host.Children)
// {
// child.Palette = v;
// }
},
};
public CUIProp<CUISprite> BackgroundSprite = new CUIProp<CUISprite>()
{
Value = CUISprite.Default,
ShowInDebug = false,
Validate = (v, host) => v ?? CUISprite.Default,
OnSet = (v, host) =>
{
if (host.ResizeToSprite)
{
host.Absolute = host.Absolute with
{
Width = v.SourceRect.Width,
Height = v.SourceRect.Height,
};
}
if (host.IgnoreTransparent)
{
Rectangle bounds = host.BackgroundSprite.Texture.Bounds;
host.TextureData = new Color[bounds.Width * bounds.Height];
host.BackgroundSprite.Texture.GetData<Color>(host.TextureData);
}
},
};
public CUIProp<bool> IgnoreTransparent = new CUIProp<bool>()
{
OnSet = (v, host) =>
{
if (v)
{
Rectangle bounds = host.BackgroundSprite.Texture.Bounds;
host.TextureData = new Color[bounds.Width * bounds.Height];
host.BackgroundSprite.Texture.GetData<Color>(host.TextureData);
}
else
{
host.TextureData = null;
}
},
};
public CUIProp<Color> BackgroundColor = new CUIProp<Color>()
{
ShowInDebug = false,
OnSet = (v, host) =>
{
host.BackgroundVisible = v != Color.Transparent;
},
};
public CUIProp<Color> OutlineColor = new CUIProp<Color>()
{
ShowInDebug = false,
OnSet = (v, host) =>
{
host.OutlineVisible = v != Color.Transparent;
},
};
public CUIProp<Vector2> Padding = new CUIProp<Vector2>()
{
Value = new Vector2(2, 2),
DecorProp = true,
};
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent : IDisposable
{
/// <summary>
/// Global ID, unique for component
/// </summary>
public int ID { get; set; }
internal bool DebugHighlight { get; set; }
private CUIMainComponent mainComponent;
/// <summary>
/// Link to CUIMainComponent, passed to children
/// </summary>
public CUIMainComponent MainComponent
{
get => mainComponent;
set
{
mainComponent = value;
foreach (var child in Children) { child.MainComponent = value; }
}
}
internal int positionalZIndex;
internal int addedZIndex;
[Calculated] public bool Focused { get; set; }
/// <summary>
/// True when parent has HideChildrenOutsideFrame and child wanders beyond parents border
/// </summary>
[Calculated] internal bool CulledOut { get; set; }
/// <summary>
/// BackgroundColor != Color.Transparent
/// </summary>
protected bool BackgroundVisible { get; set; }
protected bool OutlineVisible { get; set; }
// This is for state clones, to protect them from style changes
internal bool Unreal { get; set; }
public bool MouseOver { get; set; }
public bool MousePressed { get; set; }
/// <summary>
/// This is used by text to prevent resizing beyond that
/// and works as AbsoluteMin
/// </summary>
[Calculated]
public CUINullVector2 ForcedMinSize
{
get => forsedSize;
set => SetForcedMinSize(value);
}
protected CUINullVector2 forsedSize; internal void SetForcedMinSize(CUINullVector2 value, [CallerMemberName] string memberName = "")
{
forsedSize = value;
CUIDebug.Capture(null, this, "SetForcedMinSize", memberName, "ForcedMinSize", ForcedMinSize.ToString());
OnPropChanged();//TODO this is the reason why lists with a lot of children lag
//OnSelfAndParentChanged();
OnAbsolutePropChanged();
}
/// <summary>
/// This is set by ChildrenOffset when zooming, and iirc consumed by text to adjust text scale
/// </summary>
[Calculated]
public float Scale
{
get => scale;
set => SetScale(value);
}
protected float scale = 1f; internal void SetScale(float value, [CallerMemberName] string memberName = "")
{
scale = value;
foreach (var child in Children) { child.Scale = value; }
// OnDecorPropChanged();
}
/// <summary>
/// Calculated Prop, Real + BorderThickness
/// </summary>
protected CUIRect BorderBox { get; set; }
protected CUIRect OutlineBox { get; set; }
internal Rectangle? ScissorRect { get; set; }
/// <summary>
/// Buffer for texture data, for IgnoreTransparent checks
/// </summary>
protected Color[] TextureData;
/// <summary>
/// Calculated prop, position on real screen in pixels
/// Should be fully calculated after CUIMainComponent.Update
/// </summary>
[Calculated]
public CUIRect Real
{
get => real;
set => SetReal(value);
}
private CUIRect real; internal void SetReal(CUIRect value, [CallerMemberName] string memberName = "")
{
//HACK idk if i need it
real = new CUIRect(
(float)Math.Round(value.Left),
(float)Math.Round(value.Top),
(float)Math.Round(value.Width),
(float)Math.Round(value.Height)
);
// real = value;
CUIDebug.Capture(null, this, "SetReal", memberName, "real", real.ToString());
BorderBox = real;
// BorderBox = new CUIRect(
// real.Left - BorderThickness,
// real.Top - BorderThickness,
// real.Width + 2 * BorderThickness,
// real.Height + 2 * BorderThickness
// );
OutlineBox = new CUIRect(
real.Left - OutlineThickness,
real.Top - OutlineThickness,
real.Width + 2 * OutlineThickness,
real.Height + 2 * OutlineThickness
);
if (HideChildrenOutsideFrame)
{
Rectangle SRect = real.Box;
// //HACK Remove these + 1
// Rectangle SRect = new Rectangle(
// (int)real.Left + 1,
// (int)real.Top + 1,
// (int)real.Width - 2,
// (int)real.Height - 2
// );
if (Parent?.ScissorRect != null)
{
ScissorRect = Rectangle.Intersect(Parent.ScissorRect.Value, SRect);
}
else
{
ScissorRect = SRect;
}
}
else ScissorRect = Parent?.ScissorRect;
}
}
}

View File

@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public class CommandAttribute : System.Attribute { }
/// <summary>
/// Can be dispatched up the component tree to notify parent about something
/// add pass some event data without creating a hard link
/// </summary>
/// <param name="Name"></param>
public record CUICommand(string Name, object Data = null);
/// <summary>
/// Can be dispatched down the component tree to pass some data to the children
/// without creating a hard link
/// </summary>
public record CUIData(string Name, object Data = null);
public partial class CUIComponent
{
private void SetupCommands()
{
// This is actually expensive
//AddCommands();
OnTreeChanged += UpdateDataTargets;
}
/// <summary>
/// This command will be dispatched up when some component specific event happens
/// </summary>
[CUISerializable] public string Command { get; set; }
/// <summary>
/// this will be executed on any command
/// </summary>
public event Action<CUICommand> OnAnyCommand;
/// <summary>
/// Will be executed when receiving any data
/// </summary>
public event Action<CUIData> OnAnyData;
/// <summary>
/// Happens when appropriate data is received
/// </summary>
public event Action<Object> OnConsume;
/// <summary>
/// Will consume data with this name
/// </summary>
[CUISerializable] public string Consumes { get; set; }
private bool reflectCommands;
[CUISerializable]
public bool ReflectCommands
{
get => reflectCommands;
set
{
reflectCommands = value;
OnAnyCommand += (command) =>
{
foreach (CUIComponent child in Children)
{
child.DispatchDown(new CUIData(command.Name, command.Data));
}
};
}
}
private bool retranslateCommands;
[CUISerializable]
public bool RetranslateCommands
{
get => retranslateCommands;
set
{
retranslateCommands = value;
OnAnyCommand += (command) =>
{
Parent?.DispatchUp(command);
};
}
}
/// <summary>
/// Optimization to data flow
/// If not empty component will search for consumers of the data
/// and pass it directly to them instead of broadcasting it
/// </summary>
//[CUISerializable]
public ObservableCollection<string> Emits
{
get => emits;
set
{
emits = value;
emits.CollectionChanged += (o, e) => UpdateDataTargets();
UpdateDataTargets();
}
}
private ObservableCollection<string> emits = new();
private void UpdateDataTargets()
{
if (Emits.Count > 0)
{
DataTargets.Clear();
RunRecursiveOn(this, (c) =>
{
if (Emits.Contains(c.Consumes))
{
if (!DataTargets.ContainsKey(c.Consumes)) DataTargets[c.Consumes] = new();
DataTargets[c.Consumes].Add(c);
}
});
}
}
/// <summary>
/// Consumers of emmited data, updates on tree change
/// </summary>
public Dictionary<string, List<CUIComponent>> DataTargets = new();
/// <summary>
/// All commands
/// </summary>
public Dictionary<string, Action<object>> Commands { get; set; } = new();
/// <summary>
/// Manually adds command
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
public void AddCommand(string name, Action<object> action) => Commands.Add(name, action);
public void RemoveCommand(string name) => Commands.Remove(name);
/// <summary>
/// Executed autpmatically on component creation
/// Methods ending in "Command" will be added as commands
/// </summary>
private void AddCommands()
{
foreach (MethodInfo mi in this.GetType().GetMethods())
{
if (Attribute.IsDefined(mi, typeof(CommandAttribute)))
{
try
{
string name = mi.Name;
if (name != "Command" && name.EndsWith("Command"))
{
name = name.Substring(0, name.Length - "Command".Length);
}
AddCommand(name, mi.CreateDelegate<Action<object>>(this));
}
catch (Exception e)
{
Info($"{e.Message}\nMethod: {this.GetType()}.{mi.Name}");
}
}
}
}
/// <summary>
/// Dispathes command up the component tree until someone consumes it
/// </summary>
/// <param name="command"></param>
public void DispatchUp(CUICommand command)
{
if (OnAnyCommand != null) OnAnyCommand?.Invoke(command);
else if (Commands.ContainsKey(command.Name)) Execute(command);
else Parent?.DispatchUp(command);
}
/// <summary>
/// Dispathes command down the component tree until someone consumes it
/// </summary>
public void DispatchDown(CUIData data)
{
if (Emits.Contains(data.Name))
{
if (DataTargets.ContainsKey(data.Name))
{
foreach (CUIComponent target in DataTargets[data.Name])
{
target.OnConsume?.Invoke(data.Data);
}
}
}
else
{
if (Consumes == data.Name) OnConsume?.Invoke(data.Data);
else if (OnAnyData != null) OnAnyData.Invoke(data);
else
{
foreach (CUIComponent child in Children) child.DispatchDown(data);
}
}
}
/// <summary>
/// Will execute action corresponding to this command
/// </summary>
/// <param name="commandName"></param>
public void Execute(CUICommand command)
{
Commands.GetValueOrDefault(command.Name)?.Invoke(command.Data);
}
}
}

View File

@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Debug --------------------------------------------------------
/// <summary>
/// Mark component and its children for debug
/// Used in debug interface
/// </summary>
private bool debug; public bool Debug
{
get => debug;
set
{
debug = value;
//foreach (CUIComponent c in Children) { c.Debug = value; }
}
}
/// <summary>
/// For debug frame itself
/// </summary>
private bool ignoreDebug; public bool IgnoreDebug
{
get => ignoreDebug;
set
{
ignoreDebug = value;
foreach (CUIComponent c in Children) { c.IgnoreDebug = value; }
}
}
public void PrintTree(string offset = "")
{
CUI.Log($"{offset}{this}");
foreach (CUIComponent child in Children)
{
child.PrintTree(offset + "| ");
}
}
/// <summary>
/// Prints component and then message
/// </summary>
/// <param name="msg"></param>
/// <param name="source"></param>
/// <param name="lineNumber"></param>
public void Info(object msg, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
var fi = new FileInfo(source);
CUI.Log($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", Color.Yellow * 0.5f);
CUI.Log($"{this} {msg ?? "null"}", Color.Yellow);
}
#endregion
#region AKA --------------------------------------------------------
/// <summary>
/// Parent can memorize it's children by their names, AKA
/// </summary>
[CUISerializable] public string AKA { get; set; }
/// <summary>
/// All memorized components
/// </summary>
public Dictionary<string, CUIComponent> NamedComponents { get; set; } = new();
public CUIComponent Remember(CUIComponent c, string name)
{
NamedComponents[name] = c;
c.AKA = name;
return c;
}
/// <summary>
/// If it already has AKA
/// </summary>
public CUIComponent Remember(CUIComponent c)
{
if (c.AKA != null) NamedComponents[c.AKA] = c;
return c;
}
public CUIComponent Forget(string name)
{
if (name == null) return null;
CUIComponent c = NamedComponents.GetValueOrDefault(name);
NamedComponents.Remove(name);
return c;
}
/// <summary>
/// If it already has AKA
/// </summary>
public CUIComponent Forget(CUIComponent c)
{
if (c?.AKA != null) NamedComponents.Remove(c.AKA);
return c;
}
/// <summary>
/// You can access NamedComponents with this indexer
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public CUIComponent this[string name]
{
get => Get(name);
set
{
if (value.Parent != null) Remember(value, name);
else Append(value, name);
}
}
/// <summary>
/// Returns memorized component by name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public virtual CUIComponent Get(string name)
{
if (name == null) return null;
if (NamedComponents.ContainsKey(name)) return NamedComponents[name];
CUIComponent component = this;
string[] names = name.Split('.');
foreach (string n in names)
{
component = component.NamedComponents.GetValueOrDefault(n);
if (component == null)
{
CUI.Warning($"Failed to Get {name} from {this}, there's no {n}");
break;
}
}
return component;
}
public T Get<T>(string name) where T : CUIComponent => (T)Get(name);
#endregion
}
}

View File

@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Events --------------------------------------------------------
[CUISerializable] public bool ConsumeMouseClicks { get; set; }
[CUISerializable] public bool ConsumeDragAndDrop { get; set; }
[CUISerializable] public bool ConsumeSwipe { get; set; }
[CUISerializable] public bool ConsumeMouseScroll { get; set; }
//HACK no one will ever find it, hehehe
public void CascadeRefresh()
{
if (this is IRefreshable refreshable) refreshable.Refresh();
Children.ForEach(c => c.CascadeRefresh());
}
public event Action OnTreeChanged;
public event Action<double> OnUpdate;
public event Action<CUIInput> OnMouseLeave;
public event Action<CUIInput> OnMouseEnter;
public event Action<CUIInput> OnMouseDown;
public event Action<CUIInput> OnMouseUp;
public event Action<CUIInput> OnMouseMove;
public event Action<CUIInput> OnMouseOn;
public event Action<CUIInput> OnMouseOff;
public event Action<CUIInput> OnClick;
public event Action<CUIInput> OnDClick;
public event Action<CUIInput> OnScroll;
public event Action<float, float> OnDrag;
public event Action<float, float> OnSwipe;
public event Action<CUIInput> OnKeyDown;
public event Action<CUIInput> OnKeyUp;
public event Action<CUIInput> OnTextInput;
public event Action OnFocus;
public event Action OnFocusLost;
public Action<double> AddOnUpdate { set { OnUpdate += value; } }
public Action<CUIInput> AddOnMouseLeave { set { OnMouseLeave += value; } }
public Action<CUIInput> AddOnMouseEnter { set { OnMouseEnter += value; } }
public Action<CUIInput> AddOnMouseDown { set { OnMouseDown += value; } }
public Action<CUIInput> AddOnMouseUp { set { OnMouseUp += value; } }
public Action<CUIInput> AddOnMouseMove { set { OnMouseMove += value; } }
public Action<CUIInput> AddOnMouseOn { set { OnMouseOn += value; } }
public Action<CUIInput> AddOnMouseOff { set { OnMouseOff += value; } }
public Action<CUIInput> AddOnClick { set { OnClick += value; } }
public Action<CUIInput> AddOnDClick { set { OnDClick += value; } }
public Action<CUIInput> AddOnScroll { set { OnScroll += value; } }
public Action<float, float> AddOnDrag { set { OnDrag += value; } }
public Action<float, float> AddOnSwipe { set { OnSwipe += value; } }
public Action<CUIInput> AddOnKeyDown { set { OnKeyDown += value; } }
public Action<CUIInput> AddOnKeyUp { set { OnKeyUp += value; } }
public Action<CUIInput> AddOnTextInput { set { OnTextInput += value; } }
public Action AddOnFocus { set { OnFocus += value; } }
public Action AddOnFocusLost { set { OnFocusLost += value; } }
//TODO add more CUISpriteDrawModes
public virtual bool IsPointOnTransparentPixel(Vector2 point)
{
if (BackgroundSprite.DrawMode != CUISpriteDrawMode.Resize) return true;
//TODO hangle case where offset != sprite.origin
Vector2 RotationCenter = new Vector2(
BackgroundSprite.Offset.X * Real.Width,
BackgroundSprite.Offset.Y * Real.Height
);
Vector2 v = (point - Real.Position - RotationCenter).Rotate(-BackgroundSprite.Rotation) + RotationCenter;
float x = v.X / Real.Width;
float y = v.Y / Real.Height;
Rectangle bounds = BackgroundSprite.Texture.Bounds;
Rectangle SourceRect = BackgroundSprite.SourceRect;
int textureX = (int)Math.Round(SourceRect.X + x * SourceRect.Width);
int textureY = (int)Math.Round(SourceRect.Y + y * SourceRect.Height);
if (textureX < SourceRect.X || (SourceRect.X + SourceRect.Width - 1) < textureX) return true;
if (textureY < SourceRect.Y || (SourceRect.Y + SourceRect.Height - 1) < textureY) return true;
Color cl = TextureData[textureY * bounds.Width + textureX];
return cl.A == 0;
}
public virtual bool ShouldInvoke(CUIInput e)
{
if (IgnoreTransparent)
{
return !IsPointOnTransparentPixel(e.MousePosition);
}
return true;
}
internal void InvokeOnUpdate(double totalTime) => OnUpdate?.Invoke(totalTime);
internal void InvokeOnMouseLeave(CUIInput e) { OnMouseLeave?.Invoke(e); }
internal void InvokeOnMouseEnter(CUIInput e) { if (ShouldInvoke(e)) OnMouseEnter?.Invoke(e); }
internal void InvokeOnMouseDown(CUIInput e) { if (ShouldInvoke(e)) OnMouseDown?.Invoke(e); }
internal void InvokeOnMouseUp(CUIInput e) { if (ShouldInvoke(e)) OnMouseUp?.Invoke(e); }
internal void InvokeOnMouseMove(CUIInput e) { if (ShouldInvoke(e)) OnMouseMove?.Invoke(e); }
internal void InvokeOnMouseOn(CUIInput e) { if (ShouldInvoke(e)) OnMouseOn?.Invoke(e); }
internal void InvokeOnMouseOff(CUIInput e) { if (ShouldInvoke(e)) OnMouseOff?.Invoke(e); }
internal void InvokeOnClick(CUIInput e) { if (ShouldInvoke(e)) OnClick?.Invoke(e); }
internal void InvokeOnDClick(CUIInput e) { if (ShouldInvoke(e)) OnDClick?.Invoke(e); }
internal void InvokeOnScroll(CUIInput e) { if (ShouldInvoke(e)) OnScroll?.Invoke(e); }
internal void InvokeOnDrag(float x, float y) => OnDrag?.Invoke(x, y);
internal void InvokeOnSwipe(float x, float y) => OnSwipe?.Invoke(x, y);
internal void InvokeOnKeyDown(CUIInput e) { if (ShouldInvoke(e)) OnKeyDown?.Invoke(e); }
internal void InvokeOnKeyUp(CUIInput e) { if (ShouldInvoke(e)) OnKeyUp?.Invoke(e); }
internal void InvokeOnTextInput(CUIInput e) { if (ShouldInvoke(e)) OnTextInput?.Invoke(e); }
internal void InvokeOnFocus() => OnFocus?.Invoke();
internal void InvokeOnFocusLost() => OnFocusLost?.Invoke();
#endregion
#region Handles --------------------------------------------------------
internal CUIDragHandle DragHandle = new CUIDragHandle();
[CUISerializable]
public bool Draggable
{
get => DragHandle.Draggable;
set => DragHandle.Draggable = value;
}
//HACK Do i really need this?
internal CUIFocusHandle FocusHandle = new CUIFocusHandle();
[CUISerializable]
public bool Focusable
{
get => FocusHandle.Focusable;
set => FocusHandle.Focusable = value;
}
public CUIResizeHandle LeftResizeHandle = new CUIResizeHandle(new Vector2(0, 1), new CUIBool2(false, false));
public CUIResizeHandle RightResizeHandle = new CUIResizeHandle(new Vector2(1, 1), new CUIBool2(true, false));
public bool Resizible
{
get => ResizibleLeft || ResizibleRight;
set { ResizibleLeft = value; ResizibleRight = value; }
}
[CUISerializable]
public bool ResizibleLeft
{
get => LeftResizeHandle.Visible;
set => LeftResizeHandle.Visible = value;
}
[CUISerializable]
public bool ResizibleRight
{
get => RightResizeHandle.Visible;
set => RightResizeHandle.Visible = value;
}
[CUISerializable]
public CUIBool2 ResizeDirection
{
get => RightResizeHandle.Direction;
set
{
LeftResizeHandle.Direction = value;
RightResizeHandle.Direction = value;
}
}
internal CUISwipeHandle SwipeHandle = new CUISwipeHandle();
[CUISerializable]
public bool Swipeable
{
get => SwipeHandle.Swipeable;
set => SwipeHandle.Swipeable = value;
}
#endregion
}
}

View File

@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
/// <summary>
/// Used for text, should be in CUITextBlock really
/// </summary>
[CUISerializable]
public Vector2 Padding
{
get => CUIProps.Padding.Value;
set => CUIProps.Padding.SetValue(value);
}
/// <summary>
/// Should be one texture, not sprite sheet
/// Or there would be no way to wrap it
/// Top side will always point outwards
/// </summary>
[CUISerializable]
public CUISprite BorderSprite { get; set; } = CUISprite.Default;
/// <summary>
/// Container for Color and Thickness
/// Border is drawn inside the component and will eat space from content
/// If "by side" border prop != null then it'll take presidence
/// </summary>
[CUISerializable] public CUIBorder Border { get; set; } = new CUIBorder();
[CUISerializable] public CUIBorder TopBorder { get; set; }
[CUISerializable] public CUIBorder RigthBorder { get; set; }
[CUISerializable] public CUIBorder BottomBorder { get; set; }
[CUISerializable] public CUIBorder LeftBorder { get; set; }
[CUISerializable]
public float OutlineThickness { get; set; } = 1f;
/// <summary>
/// Outline is like a border, but on the outside of the component
/// </summary>
[CUISerializable]
public Color OutlineColor
{
get => CUIProps.OutlineColor.Value;
set => CUIProps.OutlineColor.SetValue(value);
}
/// <summary>
/// Will be drawn in background with BackgroundColor
/// Default is solid white 1x1 texture
/// </summary>
[CUISerializable]
public CUISprite BackgroundSprite
{
get => CUIProps.BackgroundSprite.Value;
set => CUIProps.BackgroundSprite.SetValue(value);
}
/// <summary>
/// If true, mouse events on transparent pixels will be ignored
/// Note: this will buffer texture data and potentially consume a lot of memory
/// so use wisely
/// </summary>
[CUISerializable]
public bool IgnoreTransparent
{
get => CUIProps.IgnoreTransparent.Value;
set => CUIProps.IgnoreTransparent.SetValue(value);
}
//TODO i think those colors could be stored inside sprites
// But then it'll be much harder to apply side effects, think about it
/// <summary>
/// Color of BackgroundSprite, default is black
/// If you're using custom sprite and don't see it make sure this color is not black
/// </summary>
[CUISerializable]
public Color BackgroundColor
{
get => CUIProps.BackgroundColor.Value;
set => CUIProps.BackgroundColor.SetValue(value);
}
private float transparency = 1.0f;
public float Transparency
{
get => transparency;
set
{
transparency = value;
foreach (CUIComponent child in Children)
{
if (!child.IgnoreParentTransparency) child.Transparency = value;
}
}
}
/// <summary>
/// This palette will be used to resolve palette styles
/// Primary, Secondary, Tertiary, Quaternary
/// </summary>
[CUISerializable]
public PaletteOrder Palette
{
get => CUIProps.Palette.Value;
set => CUIProps.Palette.SetValue(value);
}
public PaletteOrder DeepPalette
{
set
{
Palette = value;
foreach (var child in Children)
{
child.DeepPalette = value;
}
}
}
/// <summary>
/// Had to expose resize handle props, because it's not a real component
/// and can't really use styles
/// </summary>
[CUISerializable]
public Color ResizeHandleColor { get; set; } = Color.White;
[CUISerializable]
public Color ResizeHandleGrabbedColor { get; set; } = Color.Cyan;
/// <summary>
/// don't
/// </summary>
public SamplerState SamplerState { get; set; }
}
}

View File

@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
/// <summary>
/// Should children be cut off by scissor rect, this is just visual, it's not the same as culling
/// </summary>
[CUISerializable] public bool HideChildrenOutsideFrame { get; set; }
/// <summary>
/// if child rect doesn't intersect with parent it won't be drawn and won't consume fps
/// It also sets HideChildrenOutsideFrame
/// </summary>
[CUISerializable]
public bool CullChildren
{
get => CUIProps.CullChildren.Value;
set => CUIProps.CullChildren.SetValue(value);
}
/// <summary>
/// It shouldn't be culled off even outside of parent bounds and even if parent demands so
/// </summary>
[CUISerializable] public bool UnCullable { get; set; }
/// <summary>
/// Will shift all children by this much, e.g. this is how scroll works
/// It's also 3D
/// </summary>
[CUISerializable]
public CUI3DOffset ChildrenOffset
{
get => CUIProps.ChildrenOffset.Value;
set => CUIProps.ChildrenOffset.SetValue(value);
}
/// <summary>
/// Limits to children positions
/// </summary>
public Func<CUIRect, CUIBoundaries> ChildrenBoundaries { get; set; }
/// <summary>
/// Should it ignore child offset?
/// </summary>
[CUISerializable] public bool Fixed { get; set; }
/// <summary>
/// this point of this component
/// </summary>
[CUISerializable] public Vector2 Anchor { get; set; }
/// <summary>
/// will be attached to this point of parent
/// </summary>
[CUISerializable] public Vector2? ParentAnchor { get; set; }
/// <summary>
/// Ghost components don't affect layout
/// </summary>
[CUISerializable]
public CUIBool2 Ghost
{
get => CUIProps.Ghost.Value;
set => CUIProps.Ghost.SetValue(value);
}
/// <summary>
/// Components are drawn in order of their ZIndex
/// Normally it's derived from component position in the tree,
/// but this will override it
/// </summary>
[CUISerializable]
public int? ZIndex
{
get => CUIProps.ZIndex.Value;
set => CUIProps.ZIndex.SetValue(value);
}
/// <summary>
/// If true component will set it's Absolute size to sprite texture size
/// </summary>
[CUISerializable]
public bool ResizeToSprite
{
get => CUIProps.ResizeToSprite.Value;
set => CUIProps.ResizeToSprite.SetValue(value);
}
/// <summary>
/// Will be resized to fill empty space in list components
/// </summary>
[CUISerializable]
public CUIBool2 FillEmptySpace
{
get => CUIProps.FillEmptySpace.Value;
set => CUIProps.FillEmptySpace.SetValue(value);
}
/// <summary>
/// Will resize itself to fit components with absolute size, e.g. text
/// </summary>
[CUISerializable]
public CUIBool2 FitContent
{
get => CUIProps.FitContent.Value;
set => CUIProps.FitContent.SetValue(value);
}
/// <summary>
/// Absolute size and position in pixels
/// </summary>
[CUISerializable]
public CUINullRect Absolute
{
get => CUIProps.Absolute.Value;
set => CUIProps.Absolute.SetValue(value);
}
[CUISerializable]
public CUINullRect AbsoluteMin
{
get => CUIProps.AbsoluteMin.Value;
set => CUIProps.AbsoluteMin.SetValue(value);
}
[CUISerializable]
public CUINullRect AbsoluteMax
{
get => CUIProps.AbsoluteMax.Value;
set => CUIProps.AbsoluteMax.SetValue(value);
}
/// <summary>
/// Relative to parent size and position, [0..1]
/// </summary>
[CUISerializable]
public CUINullRect Relative
{
get => CUIProps.Relative.Value;
set => CUIProps.Relative.SetValue(value);
}
[CUISerializable]
public CUINullRect RelativeMin
{
get => CUIProps.RelativeMin.Value;
set => CUIProps.RelativeMin.SetValue(value);
}
[CUISerializable]
public CUINullRect RelativeMax
{
get => CUIProps.RelativeMax.Value;
set => CUIProps.RelativeMax.SetValue(value);
}
/// <summary>
/// It's like Relative, but to the opposite dimension
/// E.g. Real.Width = CrossRelative.Width * Parent.Real.Height
/// Handy for creating square things
/// </summary>
[CUISerializable]
public CUINullRect CrossRelative
{
get => CUIProps.CrossRelative.Value;
set => CUIProps.CrossRelative.SetValue(value);
}
/// <summary>
/// Used in Grid, space separated Row sizes, either in pixels (123) or in % (123%)
/// </summary>
[CUISerializable] public string GridTemplateRows { get; set; }
/// <summary>
/// Used in Grid, space separated Columns sizes, either in pixels (123) or in % (123%)
/// </summary>
[CUISerializable] public string GridTemplateColumns { get; set; }
/// <summary>
/// Component will be placed in this cell in the grid component
/// </summary>
[CUISerializable] public Point? GridStartCell { get; set; }
/// <summary>
/// And resized to fit cells from GridStartCell to GridEndCell
/// </summary>
[CUISerializable] public Point? GridEndCell { get; set; }
/// <summary>
/// Sets both GridStartCell and GridEndCell at once
/// </summary>
public Point? GridCell
{
get => GridStartCell;
set
{
GridStartCell = value;
GridEndCell = value;
}
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Layout --------------------------------------------------------
protected CUILayout layout;
//[CUISerializable]
public virtual CUILayout Layout
{
get => layout;
set { layout = value; layout.Host = this; }
}
public event Action OnLayoutUpdated;
public void InvokeOnLayoutUpdated() => OnLayoutUpdated?.Invoke();
/// <summary>
/// Triggers recalculation of layouts from parent and below
/// </summary>
internal void OnPropChanged([CallerMemberName] string memberName = "")
{
Layout.Changed = true;
CUIDebug.Capture(null, this, "OnPropChanged", memberName, "Layout.Changed", "true");
MainComponent?.LayoutChanged();
}
internal void OnSelfAndParentChanged([CallerMemberName] string memberName = "")
{
Layout.SelfAndParentChanged = true;
CUIDebug.Capture(null, this, "OnSelfAndParentChanged", memberName, "Layout.SelfAndParentChanged", "true");
MainComponent?.LayoutChanged();
}
/// <summary>
/// Triggers recalc of own pseudo components and nothing else
/// </summary>
internal void OnDecorPropChanged([CallerMemberName] string memberName = "")
{
Layout.DecorChanged = true;
CUIDebug.Capture(null, this, "OnDecorPropChanged", memberName, "Layout.DecorChanged", "true");
MainComponent?.LayoutChanged();
}
/// <summary>
/// Notifies parent (only) than it may need to ResizeToContent
/// </summary>
internal void OnAbsolutePropChanged([CallerMemberName] string memberName = "")
{
Layout.AbsoluteChanged = true;
CUIDebug.Capture(null, this, "OnAbsolutePropChanged", memberName, "Layout.AbsoluteChanged", "true");
MainComponent?.LayoutChanged();
}
/// <summary>
/// Triggers recalculation of layouts from this and below
/// </summary>
internal void OnChildrenPropChanged([CallerMemberName] string memberName = "")
{
Layout.ChildChanged = true;
MainComponent?.LayoutChanged();
}
#endregion
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
//HACK This is potentially cursed
/// <summary>
/// Arbitrary data
/// </summary>
public object Data { get; set; }
/// <summary>
/// Will prevent serialization to xml if true
/// </summary>
public bool Unserializable { get; set; }
/// <summary>
/// Is this a serialization cutoff point
/// Parent will serialize children down to this component
/// Further serialization should be hadled by this component
/// </summary>
[CUISerializable] public bool BreakSerialization { get; set; }
/// <summary>
/// Some props (like visible) are autopassed to all new childs
/// see PassPropsToChild
/// </summary>
[CUISerializable] public bool ShouldPassPropsToChildren { get; set; } = true;
/// <summary>
/// Don't inherit parent Visibility
/// </summary>
[CUISerializable] public bool IgnoreParentVisibility { get; set; }
/// <summary>
/// Don't inherit parent IgnoreEvents
/// </summary>
[CUISerializable] public bool IgnoreParentEventIgnorance { get; set; }
/// <summary>
/// Don't inherit parent ZIndex
/// </summary>
[CUISerializable] public bool IgnoreParentZIndex { get; set; }
[CUISerializable] public bool IgnoreParentTransparency { get; set; }
/// <summary>
/// Invisible components are not drawn, but still can be interacted with
/// </summary>
[CUISerializable]
public bool Visible
{
get => CUIProps.Visible.Value;
set => CUIProps.Visible.SetValue(value);
}
/// <summary>
/// Won't react to mouse events
/// </summary>
[CUISerializable]
public bool IgnoreEvents
{
get => CUIProps.IgnoreEvents.Value;
set => CUIProps.IgnoreEvents.SetValue(value);
}
/// <summary>
/// Visible + !IgnoreEvents
/// </summary>
public bool Revealed
{
get => CUIProps.Revealed.Value;
set => CUIProps.Revealed.SetValue(value);
}
//HACK this is meant for buttons, but i want to access it on generic components in CUIMap
protected bool disabled;
/// <summary>
/// Usually means - non interactable, e.g. unclickable gray button
/// </summary>
[CUISerializable]
public virtual bool Disabled
{
get => disabled;
set => disabled = value;
}
}
}

View File

@@ -0,0 +1,468 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent
{
public record CompareResult(bool equal, string firstMismatch = "")
{
public static implicit operator bool(CompareResult r) => r.equal;
}
public static bool DeepCompareVerbose(CUIComponent a, CUIComponent b)
{
CompareResult result = DeepCompare(a, b);
if (result.equal) CUI.Log($"{a} == {b}");
else CUI.Log($"{result.firstMismatch}");
return result.equal;
}
public static CompareResult DeepCompare(CUIComponent a, CUIComponent b)
{
if (a.GetType() != b.GetType()) return new CompareResult(false, $"type mismatch: {a} | {b}");
Type T = a.GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(T);
foreach (var (key, pi) in meta.Serializable)
{
if (!object.Equals(pi.GetValue(a), pi.GetValue(b)))
{
return new CompareResult(false, $"{pi}: {a}{pi.GetValue(a)} | {b}{pi.GetValue(b)}");
}
}
if (a.Children.Count != b.Children.Count)
{
return new CompareResult(false, $"child count mismatch: {a}{CUI.ArrayToString(a.Children)} | {b}{CUI.ArrayToString(b.Children)}");
}
for (int i = 0; i < a.Children.Count; i++)
{
CompareResult sub = DeepCompare(a.Children[i], b.Children[i]);
if (!sub.equal) return sub;
}
return new CompareResult(true);
}
#region State --------------------------------------------------------
/// <summary>
/// State is just a clone component with copies of all props
/// </summary>
public Dictionary<string, CUIComponent> States { get; set; } = new();
// TODO why all clones are unreal? this is sneaky, and i don't remember what's it for
public CUIComponent Clone()
{
CUIComponent clone = new CUIComponent()
{
Unreal = true,
};
clone.ApplyState(this);
return clone;
}
public void SaveStateAs(string name) => States[name] = this.Clone();
public void LoadState(string name) => ApplyState(States.GetValueOrDefault(name));
public void ForgetState(string name) => States.Remove(name);
//TODO think about edge cases (PassPropsToChild)
public void ApplyState(CUIComponent state)
{
Stopwatch sw = Stopwatch.StartNew();
if (state == null) return;
//TODO why not closest relative?
Type targetType = state.GetType() == GetType() ? GetType() : typeof(CUIComponent);
CUITypeMetaData meta = CUITypeMetaData.Get(targetType);
//TODO Megacringe, fix it
foreach (PropertyInfo pi in meta.Serializable.Values)
{
if (pi.PropertyType.IsValueType || pi.PropertyType == typeof(string))
{
pi.SetValue(this, pi.GetValue(state));
}
else
{
object value = pi.GetValue(state);
if (value == null)
{
pi.SetValue(this, null);
continue;
}
if (pi.PropertyType.IsAssignableTo(typeof(ICloneable)))
{
ICloneable cloneable = (ICloneable)pi.GetValue(state);
object clone = cloneable.Clone();
pi.SetValue(this, clone);
}
else
{
CUI.Info($"Ekhem, can't copy {pi} prop from {state} to {this} because it's not cloneable");
}
}
}
//TODO Megacringe, fix it
foreach (PropertyInfo pi in meta.Serializable.Values)
{
if (pi.PropertyType.IsValueType && !object.Equals(pi.GetValue(state), pi.GetValue(this)))
{
pi.SetValue(this, pi.GetValue(state));
}
}
}
#endregion
#region XML --------------------------------------------------------
public static bool ForceSaveAllProps { get; set; } = false;
public static bool SaveAfterLoad { get; set; } = true;
public string SavePath { get; set; }
public virtual XElement ToXML(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
if (Unserializable) return null;
Type type = GetType();
XElement e = new XElement(type.Name);
PackProps(e, propAttribute);
foreach (CUIComponent child in Children)
{
if (!this.BreakSerialization)
{
e.Add(child.ToXML(propAttribute));
}
}
return e;
}
catch (Exception e)
{
CUI.Warning(e);
return new XElement("Error", e.Message);
}
}
public virtual void FromXML(XElement element, string baseFolder = null)
{
foreach (XElement childElement in element.Elements())
{
Type childType = CUIReflection.GetComponentTypeByName(childElement.Name.ToString());
if (childType == null) continue;
CUIComponent child = (CUIComponent)Activator.CreateInstance(childType);
child.FromXML(childElement, baseFolder);
//CUI.Log($"{this}[{child.AKA}] = {child} ");
this.Append(child, child.AKA);
}
ExtractProps(element, baseFolder);
}
protected void ExtractProps(XElement element, string baseFolder = null)
{
Type type = GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(type);
foreach (XAttribute attribute in element.Attributes())
{
if (!meta.Serializable.ContainsKey(attribute.Name.ToString()))
{
CUIDebug.Error($"Can't parse prop {attribute.Name} in {type.Name} because type metadata doesn't contain that prop (is it a property? fields aren't supported yet)");
continue;
}
PropertyInfo prop = meta.Serializable[attribute.Name.ToString()];
MethodInfo parse = null;
if (CUIExtensions.Parse.ContainsKey(prop.PropertyType))
{
parse = CUIExtensions.Parse[prop.PropertyType];
}
parse ??= prop.PropertyType.GetMethod(
"Parse",
BindingFlags.Public | BindingFlags.Static,
new Type[] { typeof(string) }
);
Func<string, object> ParseWithContext = null;
//HACK
if (prop.PropertyType == typeof(CUISprite) && baseFolder != null)
{
ParseWithContext = (raw) => CUISprite.ParseWithContext(raw, baseFolder);
}
if (parse == null)
{
if (prop.PropertyType.IsEnum)
{
try
{
prop.SetValue(this, Enum.Parse(prop.PropertyType, attribute.Value));
}
catch (Exception e)
{
CUIDebug.Error($"Can't parse {attribute.Value} into {prop.PropertyType.Name}\n{e}");
}
}
else
{
CUIDebug.Error($"Can't parse prop {prop.Name} in {type.Name} because it's type {prop.PropertyType.Name} is missing Parse method");
}
}
else
{
try
{
object result = null;
if (ParseWithContext != null)
{
result = ParseWithContext(attribute.Value);
}
else
{
result = parse.Invoke(null, new object[] { attribute.Value });
}
prop.SetValue(this, result);
}
catch (Exception e)
{
CUIDebug.Error($"Can't parse {attribute.Value} into {prop.PropertyType.Name}\n{e}");
}
}
}
}
protected void PackProps(XElement element, CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
Type type = GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(type);
SortedDictionary<string, PropertyInfo> props = propAttribute switch
{
CUIAttribute.CUISerializable => meta.Serializable,
CUIAttribute.Calculated => meta.Calculated,
_ => meta.Serializable,
};
foreach (string key in props.Keys)
{
try
{
object value = props[key].GetValue(this);
// it's default value for this prop
if (!ForceSaveAllProps && meta.Default != null && Object.Equals(value, CUIReflection.GetNestedValue(meta.Default, key)))
{
continue;
}
MethodInfo customToString = CUIExtensions.CustomToString.GetValueOrDefault(props[key].PropertyType);
if (customToString != null)
{
element?.SetAttributeValue(key, customToString.Invoke(null, new object[] { value }));
}
else
{
element?.SetAttributeValue(key, value);
}
}
catch (Exception e)
{
CUI.Warning($"Failed to serialize prop: {e.Message}");
CUI.Warning($"{key} in {this}");
}
}
}
public string Serialize(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
XElement e = this.ToXML(propAttribute);
return e.ToString();
}
catch (Exception e)
{
CUI.Error(e);
return e.Message;
}
}
public static CUIComponent Deserialize(string raw, string baseFolder = null)
{
return Deserialize(XElement.Parse(raw));
}
public static CUIComponent Deserialize(XElement e, string baseFolder = null)
{
try
{
Type type = CUIReflection.GetComponentTypeByName(e.Name.ToString());
if (type == null) return null;
CUIComponent c = (CUIComponent)Activator.CreateInstance(type);
// c.RemoveAllChildren();
c.FromXML(e, baseFolder);
CUIComponent.RunRecursiveOn(c, (component) => component.Hydrate());
return c;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public void LoadSelfFromFile(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false)
{
try
{
XDocument xdoc = XDocument.Load(path);
RemoveAllChildren();
if (searchForSpritesInTheSameFolder) FromXML(xdoc.Root, Path.GetDirectoryName(path));
else FromXML(xdoc.Root);
CUIComponent.RunRecursiveOn(this, (component) => component.Hydrate());
SavePath = path;
if (SaveAfterLoad && saveAfterLoad) SaveToTheSamePath();
}
catch (Exception ex)
{
CUI.Warning(ex);
}
}
public static CUIComponent LoadFromFile(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false)
{
try
{
XDocument xdoc = XDocument.Load(path);
CUIComponent result;
if (searchForSpritesInTheSameFolder)
{
result = Deserialize(xdoc.Root, Path.GetDirectoryName(path));
}
else result = Deserialize(xdoc.Root);
result.SavePath = path;
if (SaveAfterLoad && saveAfterLoad) result.SaveToTheSamePath();
return result;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public static T LoadFromFile<T>(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false) where T : CUIComponent
{
try
{
XDocument xdoc = XDocument.Load(path);
T result;
if (searchForSpritesInTheSameFolder)
{
result = (T)Deserialize(xdoc.Root, Path.GetDirectoryName(path));
}
else result = (T)Deserialize(xdoc.Root);
result.SavePath = path;
if (SaveAfterLoad && saveAfterLoad) result.SaveToTheSamePath();
return result;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public void LoadFromTheSameFile()
{
if (SavePath == null)
{
CUI.Warning($"Can't load {this} from The Same Path, SavePath is null");
return;
}
LoadSelfFromFile(SavePath);
}
public void SaveToTheSamePath()
{
if (SavePath == null)
{
CUI.Warning($"Can't save {this} To The Same Path, SavePath is null");
return;
}
SaveToFile(SavePath);
}
public void SaveToFile(string path, CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
XDocument xdoc = new XDocument();
xdoc.Add(this.ToXML(propAttribute));
xdoc.Save(path);
SavePath = path;
}
catch (Exception e)
{
CUI.Warning(e);
}
}
/// <summary>
/// Experimental method
/// Here you can add data/ callbacks/ save stuff to variables
/// after loading a xml skeletom
/// </summary>
public virtual void Hydrate()
{
}
#endregion
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent : IDisposable
{
private void SetupStyles()
{
Style = new CUIStyle();
}
/// <summary>
/// Use it to e.g. update component color
/// </summary>
public event Action OnStyleApplied;
internal void InvokeOnStyleApplied() => OnStyleApplied?.Invoke();
private void HandleStylePropChange(string key, string value)
{
CUIGlobalStyleResolver.OnComponentStylePropChanged(this, key);
}
private void HandleStyleChange(CUIStyle s)
{
CUIGlobalStyleResolver.OnComponentStyleChanged(this);
}
private CUIStyle style;
/// <summary>
/// Allows you to assing parsable string or link to CUIPalette to any prop
/// It's indexable, so you can access it like this: component.Style["BackgroundColor"] = "cyan"
/// if value starts with "CUIPalette." it will extract the value from palette
/// e.g. component.Style["BackgroundColor"] = "CUIPalette.DarkBlue.Secondary.On"
/// </summary>
[CUISerializable]
public CUIStyle Style
{
get => style;
set
{
if (style == value) return;
if (style != null)
{
style.OnUse -= HandleStyleChange;
style.OnPropChanged -= HandleStylePropChange;
}
style = value;
if (style != null)
{
style.OnUse += HandleStyleChange;
style.OnPropChanged += HandleStylePropChange;
}
HandleStyleChange(style);
}
}
public CUIStyle ResolvedStyle { get; set; }
}
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Tree --------------------------------------------------------
public List<CUIComponent> Children { get; set; } = new();
private CUIComponent? parent; public CUIComponent? Parent
{
get => parent;
set => SetParent(value);
}
internal void SetParent(CUIComponent? value, [CallerMemberName] string memberName = "")
{
if (parent != null)
{
TreeChanged = true;
OnPropChanged();
parent.Forget(this);
parent.Children.Remove(this);
parent.OnChildRemoved?.Invoke(this);
}
parent = value;
CUIDebug.Capture(null, this, "SetParent", memberName, "parent", $"{parent}");
if (parent != null)
{
if (parent is CUIMainComponent main) MainComponent = main;
if (parent?.MainComponent != null) MainComponent = parent.MainComponent;
//parent.Children.Add(this);
TreeChanged = true;
if (AKA != null) parent.Remember(this, AKA);
parent.PassPropsToChild(this);
OnPropChanged();
parent.OnChildAdded?.Invoke(this);
}
}
private bool treeChanged = true; internal bool TreeChanged
{
get => treeChanged;
set
{
treeChanged = value;
if (value)
{
OnTreeChanged?.Invoke();
if (Parent != null) Parent.TreeChanged = true;
}
}
}
/// <summary>
/// Allows you to add array of children
/// </summary>
public IEnumerable<CUIComponent> AddChildren
{
set
{
foreach (CUIComponent c in value) { Append(c); }
}
}
public event Action<CUIComponent> OnChildAdded;
public event Action<CUIComponent> OnChildRemoved;
/// <summary>
/// Adds children to the end of the list
/// </summary>
/// <param name="child"></param>
/// <param name="name"> AKA </param>
/// <returns> child </returns>
public virtual CUIComponent Append(CUIComponent child, string name = null, [CallerMemberName] string memberName = "")
{
if (child == null) return child;
child.Parent = this;
Children.Add(child);
if (name != null) Remember(child, name);
return child;
}
/// <summary>
/// Adds children to the begining of the list
/// </summary>
/// <param name="child"></param>
/// <param name="name"> AKA </param>
/// <returns> child </returns>
public virtual CUIComponent Prepend(CUIComponent child, string name = null, [CallerMemberName] string memberName = "")
{
if (child == null) return child;
child.Parent = this;
Children.Insert(0, child);
if (name != null) Remember(child, name);
return child;
}
public virtual CUIComponent Insert(CUIComponent child, int index, string name = null, [CallerMemberName] string memberName = "")
{
if (child == null) return child;
child.Parent = this;
index = Math.Clamp(index, 0, Children.Count);
Children.Insert(index, child);
if (name != null) Remember(child, name);
return child;
}
//TODO DRY
public void RemoveSelf() => Parent?.RemoveChild(this);
public CUIComponent RemoveChild(CUIComponent child, [CallerMemberName] string memberName = "")
{
if (child == null || !Children.Contains(child)) return child;
if (this != null) // kek
{
child.TreeChanged = true;
child.OnPropChanged();
//HACK i'm sure it doesn't belong here, find a better place
forsedSize = new CUINullVector2();
OnAbsolutePropChanged();
// Forget(child);
Children.Remove(child);
OnChildRemoved?.Invoke(child);
}
child.parent = null;
CUIDebug.Capture(null, this, "RemoveChild", memberName, "child", $"{child}");
return child;
}
//TODO DRY
public void RemoveAllChildren([CallerMemberName] string memberName = "")
{
foreach (CUIComponent c in Children)
{
if (this != null) // kek
{
c.TreeChanged = true;
c.OnPropChanged();
//Forget(c);
//Children.Remove(c);
OnChildRemoved?.Invoke(c);
}
c.parent = null;
CUIDebug.Capture(null, this, "RemoveAllChildren", memberName, "child", $"{c}");
}
NamedComponents.Clear();
Children.Clear();
}
/// <summary>
/// Pass props like ZIndex, Visible to a new child
/// </summary>
/// <param name="child"></param>
protected virtual void PassPropsToChild(CUIComponent child)
{
if (!ShouldPassPropsToChildren) return;
//child.Palette = Palette;
if (ZIndex.HasValue && !child.IgnoreParentZIndex) child.ZIndex = ZIndex.Value + 1;
if (IgnoreEvents && !child.IgnoreParentEventIgnorance) child.IgnoreEvents = true;
if (!Visible && !child.IgnoreParentVisibility) child.Visible = false;
}
#endregion
}
}

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
using System.Threading;
namespace QICrabUI
{
/// <summary>
/// Base class for all components
/// </summary>
public partial class CUIComponent : IDisposable
{
#region Static --------------------------------------------------------
internal static void InitStatic()
{
CUI.OnInit += () =>
{
MaxID = 0;
};
CUI.OnDispose += () =>
{
foreach (int id in ComponentsById.Keys)
{
CUIComponent component = null;
ComponentsById[id].TryGetTarget(out component);
component?.Dispose();
}
ComponentsById.Clear();
ComponentsByType.Clear();
dummyComponent = null;
};
}
internal static int MaxID;
public static Dictionary<int, WeakReference<CUIComponent>> ComponentsById = new();
public static WeakCatalog<Type, CUIComponent> ComponentsByType = new();
/// <summary>
/// This is used to trick vanilla GUI into believing that
/// mouse is hovering some component and block clicks
/// </summary>
public static GUIButton dummyComponent = new GUIButton(new RectTransform(new Point(0, 0)))
{
Text = "DUMMY",
};
/// <summary>
/// designed to be versatile, in fact never used
/// </summary>
public static void RunRecursiveOn(CUIComponent component, Action<CUIComponent> action)
{
action(component);
foreach (CUIComponent child in component.Children)
{
RunRecursiveOn(child, action);
}
}
public static void ForEach(Action<CUIComponent> action)
{
foreach (int id in ComponentsById.Keys)
{
CUIComponent component = null;
ComponentsById[id].TryGetTarget(out component);
if (component is not null) action(component);
}
}
public static IEnumerable<Type> GetClassHierarchy(Type type)
{
while (type != typeof(Object) && type != null)
{
yield return type;
type = type.BaseType;
}
}
public static IEnumerable<Type> GetReverseClassHierarchy(Type type)
=> CUIComponent.GetClassHierarchy(type).Reverse<Type>();
#endregion
#region Virtual --------------------------------------------------------
//TODO move to cui props, it's a bit more clampicated than ChildrenBoundaries
/// <summary>
/// Bounds for offset, e.g. scroll, zoom
/// </summary>
internal virtual CUIBoundaries ChildOffsetBounds => new CUIBoundaries();
/// <summary>
/// "Component like" ghost stuff that can't have children and
/// doesn't impact layout. Drag handles, text etc
/// </summary>
internal virtual void UpdatePseudoChildren()
{
LeftResizeHandle.Update();
RightResizeHandle.Update();
}
/// <summary>
/// Last chance to disagree with proposed size
/// For stuff that should resize to content
/// </summary>
/// <param name="size"> proposed size </param>
/// <returns> size you're ok with </returns>
internal virtual Vector2 AmIOkWithThisSize(Vector2 size) => size;
/// <summary>
/// Here component should be drawn
/// </summary>
/// <param name="spriteBatch"></param>
public virtual partial void Draw(SpriteBatch spriteBatch);
/// <summary>
/// Method for drawing something that should always be on top, e.g. resize handles
/// </summary>
/// <param name="spriteBatch"></param>
public virtual partial void DrawFront(SpriteBatch spriteBatch);
#endregion
#region Draw --------------------------------------------------------
public virtual partial void Draw(SpriteBatch spriteBatch)
{
if (BackgroundVisible) CUI.DrawRectangle(spriteBatch, Real, BackgroundColor * Transparency, BackgroundSprite);
CUI.DrawBorders(spriteBatch, this);
// if (Border.Visible) GUI.DrawRectangle(spriteBatch, BorderBox.Position, BorderBox.Size, Border.Color, thickness: Border.Thickness);
if (OutlineVisible) GUI.DrawRectangle(spriteBatch, OutlineBox.Position, OutlineBox.Size, OutlineColor, thickness: OutlineThickness);
LeftResizeHandle.Draw(spriteBatch);
RightResizeHandle.Draw(spriteBatch);
}
public virtual partial void DrawFront(SpriteBatch spriteBatch)
{
if (DebugHighlight)
{
GUI.DrawRectangle(spriteBatch, Real.Position, Real.Size, Color.Cyan * 0.5f, isFilled: true);
}
}
#endregion
#region Constructors --------------------------------------------------------
internal void Vitalize()
{
foreach (FieldInfo fi in this.GetType().GetFields(AccessTools.all))
{
if (fi.FieldType.IsAssignableTo(typeof(ICUIVitalizable)))
{
ICUIVitalizable prop = (ICUIVitalizable)fi.GetValue(this);
if (prop == null) continue;
prop.SetHost(this);
}
}
}
internal void VitalizeProps()
{
foreach (FieldInfo fi in this.GetType().GetFields(AccessTools.all))
{
if (fi.FieldType.IsAssignableTo(typeof(ICUIProp)))
{
ICUIProp prop = (ICUIProp)fi.GetValue(this);
if (prop == null) continue; // this is for Main.GrabbedDragHandle
prop.SetHost(this);
prop.SetName(fi.Name);
}
}
foreach (FieldInfo fi in typeof(CUIComponentProps).GetFields(AccessTools.all))
{
if (fi.FieldType.IsAssignableTo(typeof(ICUIProp)))
{
ICUIProp prop = (ICUIProp)fi.GetValue(CUIProps);
if (prop == null) continue;
prop.SetHost(this);
prop.SetName(fi.Name);
}
}
}
public CUIComponent()
{
if (CUI.Disposed)
{
Disposed = true;
return;
}
ID = MaxID++;
ComponentsById[ID] = new WeakReference<CUIComponent>(this);
ComponentsByType.Add(this.GetType(), this);
Vitalize();
VitalizeProps();
SetupCommands();
Layout = new CUILayoutSimple();
SetupStyles();
SetupAnimations();
}
public CUIComponent(float? x = null, float? y = null, float? w = null, float? h = null) : this()
{
Relative = new CUINullRect(x, y, w, h);
}
public bool Disposed;
public void Dispose()
{
if (Disposed) return;
CleanUp();
Disposed = true;
}
public virtual void CleanUp() { }
~CUIComponent() => Dispose();
public override string ToString() => $"{this.GetType().Name}:{ID}:{AKA}";
#endregion
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
/// <summary>
/// Drop down list, aka Select
/// </summary>
public class CUIDropDown : CUIComponent
{
internal class DDOption : CUIButton
{
public DDOption() : this("") { }
public DDOption(string text) : base(text) { }
}
private CUIButton MainButton;
private CUIVerticalList OptionBox;
/// <summary>
/// List of options
/// Options are just strings
/// </summary>
[CUISerializable]
public IEnumerable<string> Options
{
get => OptionBox.Children.Cast<DDOption>().Select(o => o.Text);
set
{
Clear();
foreach (string option in value) { Add(option); }
}
}
[CUISerializable]
public string Selected
{
get => MainButton.Text;
set => Select(value);
}
public event Action<string> OnSelect;
public Action<string> AddOnSelect { set { OnSelect += value; } }
public void Open() => OptionBox.Revealed = true;
public void Close() => OptionBox.Revealed = false;
public void Clear()
{
OptionBox.RemoveAllChildren();
Select("");
}
public void Add(string option)
{
OptionBox.Append(new DDOption(option)
{
AddOnMouseDown = (e) => Select(option),
});
}
public void Select(int i) => Select(Options.ElementAtOrDefault(i));
public void Select(string option)
{
MainButton.Text = option ?? "";
OptionBox.Revealed = false;
OnSelect?.Invoke(MainButton.Text);
}
public void Remove(int i) => Remove(Options.ElementAtOrDefault(i));
public void Remove(string option)
{
if (option == null) return;
if (!Options.Contains(option)) return;
DDOption ddoption = OptionBox.Children.Cast<DDOption>().FirstOrDefault(o => o.Text == option);
bool wasSelected = MainButton.Text == ddoption.Text;
OptionBox.RemoveChild(ddoption);
if (wasSelected) Select(0);
}
public CUIDropDown() : base()
{
BreakSerialization = true;
OptionBox = new CUIVerticalList()
{
Relative = new CUINullRect(w: 1),
FitContent = new CUIBool2(true, true),
Ghost = new CUIBool2(false, true),
Anchor = CUIAnchor.TopLeft,
ParentAnchor = CUIAnchor.BottomLeft,
ZIndex = 500,
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.DDOption.Background"},
{"Border", "CUIPalette.DDOption.Border"},
},
};
MainButton = new CUIButton()
{
Text = "CUIDropDown",
Relative = new CUINullRect(w: 1, h: 1),
AddOnMouseDown = (e) => OptionBox.Revealed = !OptionBox.Revealed,
};
Append(MainButton);
Append(OptionBox);
FitContent = new CUIBool2(true, true);
//HACK Why this main is hardcoded?
//in static constructor CUI.Main is null and this won't work
if (CUI.Main is not null) CUI.Main.OnMouseDown += (e) => Close();
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Draggable and resizable container for other components
/// </summary>
public class CUIFrame : CUIComponent
{
public override void Draw(SpriteBatch spriteBatch)
{
if (BackgroundVisible) CUI.DrawRectangle(spriteBatch, Real, BackgroundColor, BackgroundSprite);
}
public override void DrawFront(SpriteBatch spriteBatch)
{
//if (BorderVisible) CUI.DrawBorders(spriteBatch, Real, BorderColor, BorderSprite, BorderThickness);
// GUI.DrawRectangle(spriteBatch, BorderBox.Position, BorderBox.Size, BorderColor, thickness: BorderThickness);
CUI.DrawBorders(spriteBatch, this);
if (OutlineVisible) GUI.DrawRectangle(spriteBatch, OutlineBox.Position, OutlineBox.Size, OutlineColor, thickness: OutlineThickness);
LeftResizeHandle.Draw(spriteBatch);
RightResizeHandle.Draw(spriteBatch);
//base.DrawFront(spriteBatch);
}
public event Action OnOpen;
public event Action OnClose;
/// <summary>
/// This will reveal the frame and append it to CUI.Main
/// </summary>
public void Open()
{
if (CUI.Main == null && Parent != CUI.Main) return;
CUI.Main.Append(this);
Revealed = true;
OnOpen?.Invoke();
}
/// <summary>
/// This will hide the frame and remove it from children of CUI.Main
/// </summary>
public void Close()
{
RemoveSelf();
Revealed = false;
OnClose?.Invoke();
}
public CUIFrame() : base()
{
CullChildren = true;
Resizible = true;
Draggable = true;
}
public CUIFrame(float? x = null, float? y = null, float? w = null, float? h = null) : this()
{
Relative = new CUINullRect(x, y, w, h);
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// A Grid containing children in its cells
/// </summary>
public class CUIGrid : CUIComponent
{
public override CUILayout Layout
{
get => layout;
set
{
layout = new CUILayoutGrid();
layout.Host = this;
}
}
public CUILayoutGrid GridLayout => (CUILayoutGrid)Layout;
public CUIGrid() : base()
{
//Layout = new CUILayoutGrid();
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Resizing components to it's Height and placing them sequentially
/// </summary>
public class CUIHorizontalList : CUIComponent
{
[CUISerializable] public bool Scrollable { get; set; }
[CUISerializable] public float ScrollSpeed { get; set; } = 1.0f;
public float LeftGap = 0f;
public float RightGap = 0f;
public override CUILayout Layout
{
get => layout;
set
{
layout = new CUILayoutHorizontalList();
layout.Host = this;
}
}
public CUILayoutHorizontalList ListLayout => (CUILayoutHorizontalList)Layout;
[CUISerializable]
public CUIDirection Direction
{
get => ListLayout.Direction;
set => ListLayout.Direction = value;
}
[CUISerializable]
public bool ResizeToHostHeight
{
get => ListLayout.ResizeToHostHeight;
set => ListLayout.ResizeToHostHeight = value;
}
public float Scroll
{
get => ChildrenOffset.X;
set
{
if (!Scrollable) return;
CUIProps.ChildrenOffset.SetValue(
ChildrenOffset with { X = value }
);
}
}
internal override CUIBoundaries ChildOffsetBounds => new CUIBoundaries(
minY: 0,
maxY: 0,
minX: LeftGap,
maxX: Math.Min(Real.Width - ListLayout.TotalWidth - RightGap, 0)
);
public CUIHorizontalList() : base()
{
CullChildren = true;
OnScroll += (m) => Scroll += m.Scroll * ScrollSpeed;
ChildrenBoundaries = CUIBoundaries.HorizontalTube;
}
}
}

View File

@@ -0,0 +1,522 @@
#define SHOWPERF
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Diagnostics;
namespace QICrabUI
{
/// <summary>
/// Orchestrating drawing and updating of it's children
/// Also a CUIComponent, but it's draw and update methods
/// Attached directly to games life cycle
/// </summary>
public class CUIMainComponent : CUIComponent
{
/// <summary>
/// Wrapper for global events
/// </summary>
public class CUIGlobalEvents
{
public Action<CUIInput> OnMouseDown; public void InvokeOnMouseDown(CUIInput e) => OnMouseDown?.Invoke(e);
public Action<CUIInput> OnMouseUp; public void InvokeOnMouseUp(CUIInput e) => OnMouseUp?.Invoke(e);
public Action<CUIInput> OnMouseMoved; public void InvokeOnMouseMoved(CUIInput e) => OnMouseMoved?.Invoke(e);
public Action<CUIInput> OnClick; public void InvokeOnClick(CUIInput e) => OnClick?.Invoke(e);
public Action<CUIInput> OnKeyDown; public void InvokeOnKeyDown(CUIInput e) => OnKeyDown?.Invoke(e);
public Action<CUIInput> OnKeyUp; public void InvokeOnKeyUp(CUIInput e) => OnKeyUp?.Invoke(e);
}
/// <summary>
/// Frozen window doesn't update
/// </summary>
public bool Frozen { get; set; }
public double UpdateInterval = 1.0 / 300.0;
/// <summary>
/// If true will update layout until it settles to prevent blinking
/// </summary>
public bool CalculateUntilResolved = true;
/// <summary>
/// If your GUI needs more than this steps of layout update
/// you will get a warning
/// </summary>
public int MaxLayoutRecalcLoopsPerUpdate = 10;
public event Action OnTreeChanged;
public Action AddOnTreeChanged { set { OnTreeChanged += value; } }
public CUIDragHandle GrabbedDragHandle;
public CUIResizeHandle GrabbedResizeHandle;
public CUISwipeHandle GrabbedSwipeHandle;
public CUIComponent MouseOn;
public CUIComponent FocusedComponent
{
get => CUI.FocusedComponent;
set => CUI.FocusedComponent = value;
}
/// <summary>
/// Container for true global events
/// CUIMainComponent itself can react to events and you can listen for those,
/// but e.g. mouse events may be consumed before they reach Main
/// </summary>
public CUIGlobalEvents Global = new CUIGlobalEvents();
private Stopwatch sw = new Stopwatch();
internal List<CUIComponent> Flat = new List<CUIComponent>();
internal List<CUIComponent> Leaves = new List<CUIComponent>();
internal SortedList<int, List<CUIComponent>> Layers = new SortedList<int, List<CUIComponent>>();
private List<CUIComponent> MouseOnList = new List<CUIComponent>();
private Vector2 GrabbedOffset;
private void RunStraigth(Action<CUIComponent> a) { for (int i = 0; i < Flat.Count; i++) a(Flat[i]); }
private void RunReverse(Action<CUIComponent> a) { for (int i = Flat.Count - 1; i >= 0; i--) a(Flat[i]); }
private void FlattenTree()
{
int retries = 0;
bool done = false;
do
{
retries++;
if (retries > 10) break;
try
{
Flat.Clear();
Layers.Clear();
int globalIndex = 0;
void CalcZIndexRec(CUIComponent component, int added = 0)
{
component.positionalZIndex = globalIndex;
globalIndex += 1;
component.addedZIndex = added;
if (component.ZIndex.HasValue) component.addedZIndex += component.ZIndex.Value;
foreach (CUIComponent child in component.Children)
{
CalcZIndexRec(child, component.addedZIndex);
}
}
CalcZIndexRec(this, 0);
RunRecursiveOn(this, (c) =>
{
int i = c.positionalZIndex + c.addedZIndex;
if (!Layers.ContainsKey(i)) Layers[i] = new List<CUIComponent>();
Layers[i].Add(c);
});
foreach (var layer in Layers)
{
Flat.AddRange(layer.Value);
}
done = true;
}
catch (Exception e)
{
CUI.Warning($"Couldn't Flatten component tree: {e.Message}");
}
} while (!done);
}
#region Update
internal bool GlobalLayoutChanged;
internal void LayoutChanged() => GlobalLayoutChanged = true;
private double LastUpdateTime;
private int UpdateLoopCount = 0;
/// <summary>
/// Forses 1 layout update step, even when Frozen
/// </summary>
public void Step()
{
Update(LastUpdateTime + UpdateInterval, true, true);
}
public void Update(double totalTime, bool force = false, bool noInput = false)
{
if (!force)
{
if (Frozen) return;
if (totalTime - LastUpdateTime <= UpdateInterval) return;
}
CUIDebug.Flush();
if (TreeChanged)
{
OnTreeChanged?.Invoke();
FlattenTree();
TreeChanged = false;
}
if (!noInput) HandleInput(totalTime);
RunStraigth(c => c.InvokeOnUpdate(totalTime));
if (CalculateUntilResolved)
{
UpdateLoopCount = 0;
do
{
GlobalLayoutChanged = false;
if (TreeChanged)
{
OnTreeChanged?.Invoke();
FlattenTree();
TreeChanged = false;
}
RunReverse(c =>
{
c.Layout.ResizeToContent();
});
RunStraigth(c =>
{
c.Layout.Update();
c.Layout.UpdateDecor();
});
UpdateLoopCount++;
if (UpdateLoopCount >= MaxLayoutRecalcLoopsPerUpdate)
{
PrintRecalLimitWarning();
break;
}
}
while (GlobalLayoutChanged);
//CUI.Log($"UpdateLoopCount: {UpdateLoopCount}");
}
else
{
RunReverse(c =>
{
c.Layout.ResizeToContent();
});
RunStraigth(c =>
{
c.Layout.Update();
c.Layout.UpdateDecor();
});
}
//TODO do i need 2 updates?
//RunStraigth(c => c.InvokeOnUpdate(totalTime));
LastUpdateTime = totalTime;
}
#endregion
#region Draw
private void StopStart(SpriteBatch spriteBatch, Rectangle SRect, SamplerState? samplerState = null)
{
samplerState ??= GUI.SamplerState;
spriteBatch.End();
spriteBatch.GraphicsDevice.ScissorRectangle = SRect;
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: samplerState, rasterizerState: GameMain.ScissorTestEnable);
}
public new void Draw(SpriteBatch spriteBatch)
{
sw.Restart();
Rectangle OriginalSRect = spriteBatch.GraphicsDevice.ScissorRectangle;
Rectangle SRect = OriginalSRect;
try
{
RunStraigth(c =>
{
if (!c.Visible || c.CulledOut) return;
if (c.Parent != null && c.Parent.ScissorRect.HasValue && SRect != c.Parent.ScissorRect.Value)
{
SRect = c.Parent.ScissorRect.Value;
StopStart(spriteBatch, SRect, c.SamplerState);
}
c.Draw(spriteBatch);
});
}
finally
{
if (spriteBatch.GraphicsDevice.ScissorRectangle != OriginalSRect) StopStart(spriteBatch, OriginalSRect);
}
RunStraigth(c =>
{
if (!c.Visible || c.CulledOut) return;
c.DrawFront(spriteBatch);
});
sw.Stop();
// CUIDebug.EnsureCategory();
// CUIDebug.CaptureTicks(sw.ElapsedTicks, "CUI.Draw");
}
#endregion
// https://youtu.be/xuFgUmYCS8E?feature=shared&t=72
#region HandleInput Start
public void OnDragEnd(CUIDragHandle h) { if (h == GrabbedDragHandle) GrabbedDragHandle = null; }
public void OnResizeEnd(CUIResizeHandle h) { if (h == GrabbedResizeHandle) GrabbedResizeHandle = null; }
public void OnSwipeEnd(CUISwipeHandle h) { if (h == GrabbedSwipeHandle) GrabbedSwipeHandle = null; }
private void HandleInput(double totalTime)
{
HandleGlobal(totalTime);
HandleMouse(totalTime);
HandleKeyboard(totalTime);
}
private void HandleGlobal(double totalTime)
{
if (CUI.Input.MouseDown) Global.InvokeOnMouseDown(CUI.Input);
if (CUI.Input.MouseUp)
{
Global.InvokeOnMouseUp(CUI.Input);
Global.InvokeOnClick(CUI.Input);
}
if (CUI.Input.MouseMoved) Global.InvokeOnMouseMoved(CUI.Input);
if (CUI.Input.SomeKeyPressed) Global.InvokeOnKeyDown(CUI.Input);
if (CUI.Input.SomeKeyUnpressed) Global.InvokeOnKeyUp(CUI.Input);
}
private void HandleKeyboard(double totalTime)
{
if (FocusedComponent == null) FocusedComponent = this;
if (CUI.Input.PressedKeys.Contains(Keys.Escape)) FocusedComponent = this;
if (CUI.Input.SomeKeyPressed) FocusedComponent.InvokeOnKeyDown(CUI.Input);
if (CUI.Input.SomeKeyUnpressed) FocusedComponent.InvokeOnKeyUp(CUI.Input);
if (CUI.Input.SomeWindowEvents) FocusedComponent.InvokeOnTextInput(CUI.Input);
}
private void HandleMouse(double totalTime)
{
if (!CUI.Input.SomethingHappened) return;
if (!CUI.Input.MouseHeld)
{
GrabbedDragHandle?.EndDrag();
GrabbedResizeHandle?.EndResize();
GrabbedSwipeHandle?.EndSwipe();
}
if (CUI.Input.MouseMoved)
{
GrabbedDragHandle?.DragTo(CUI.Input.MousePosition);
GrabbedResizeHandle?.Resize(CUI.Input.MousePosition);
GrabbedSwipeHandle?.Swipe(CUI.Input);
}
if (CUI.Input.MouseInputHandled) return;
//HACK
//if (CUI.Input.ClickConsumed) return;
//TODO think where should i put it?
if (GrabbedResizeHandle != null || GrabbedDragHandle != null || GrabbedSwipeHandle != null) return;
List<CUIComponent> prevMouseOnList = new List<CUIComponent>(MouseOnList);
CUIComponent CurrentMouseOn = null;
MouseOnList.Clear();
// form MouseOnList
// Note: including main component
if (
GUI.MouseOn == null || (GUI.MouseOn is GUIButton btn && btn.Text == "DUMMY")
|| (this == CUI.TopMain) //TODO guh
)
{
RunStraigth(c =>
{
bool ok = !c.IgnoreEvents && c.Real.Contains(CUI.Input.MousePosition) && c.ShouldInvoke(CUI.Input);
if (c.Parent != null && c.Parent.ScissorRect.HasValue &&
!c.Parent.ScissorRect.Value.Contains(CUI.Input.Mouse.Position))
{
ok = false;
}
if (ok) MouseOnList.Add(c);
});
}
MouseOn = MouseOnList.LastOrDefault();
//HACK
if (MouseOn != this)
{
CUI.Input.MouseInputHandled = true;
CUIMultiModResolver.MarkOtherInputsAsHandled();
}
//if (CurrentMouseOn != null) GUI.MouseOn = dummyComponent;
foreach (CUIComponent c in prevMouseOnList)
{
c.MousePressed = false;
c.MouseOver = false;
c.InvokeOnMouseOff(CUI.Input);
}
foreach (CUIComponent c in MouseOnList)
{
c.MousePressed = CUI.Input.MouseHeld;
c.MouseOver = true;
c.InvokeOnMouseOn(CUI.Input);
}
// Mouse enter / leave
foreach (CUIComponent c in prevMouseOnList.Except(MouseOnList)) c.InvokeOnMouseLeave(CUI.Input);
foreach (CUIComponent c in MouseOnList.Except(prevMouseOnList)) c.InvokeOnMouseEnter(CUI.Input);
// focus
if (CUI.Input.MouseDown)
{
CUIComponent newFocused = this;
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].FocusHandle.ShouldStart(CUI.Input))
{
newFocused = MouseOnList[i];
break;
}
}
FocusedComponent = newFocused;
}
// Resize
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].RightResizeHandle.ShouldStart(CUI.Input))
{
GrabbedResizeHandle = MouseOnList[i].RightResizeHandle;
GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
break;
}
if (MouseOnList[i].LeftResizeHandle.ShouldStart(CUI.Input))
{
GrabbedResizeHandle = MouseOnList[i].LeftResizeHandle;
GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
break;
}
}
if (GrabbedResizeHandle != null) return;
//Scroll
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (CUI.Input.Scrolled) MouseOnList[i].InvokeOnScroll(CUI.Input);
if (MouseOnList[i].ConsumeMouseScroll) break;
}
//Move
if (CUI.Input.MouseMoved)
{
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
MouseOnList[i].InvokeOnMouseMove(CUI.Input);
}
}
//Clicks
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (CUI.Input.MouseDown) MouseOnList[i].InvokeOnMouseDown(CUI.Input);
if (CUI.Input.MouseUp)
{
MouseOnList[i].InvokeOnMouseUp(CUI.Input);
MouseOnList[i].InvokeOnClick(CUI.Input);
}
if (CUI.Input.DoubleClick) MouseOnList[i].InvokeOnDClick(CUI.Input);
if (MouseOnList[i].ConsumeMouseClicks || CUI.Input.ClickConsumed) break;
}
if (CUI.Input.ClickConsumed) return;
// Swipe
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].SwipeHandle.ShouldStart(CUI.Input))
{
GrabbedSwipeHandle = MouseOnList[i].SwipeHandle;
GrabbedSwipeHandle.BeginSwipe(CUI.Input.MousePosition);
break;
}
if (MouseOnList[i].ConsumeSwipe) break;
}
if (GrabbedSwipeHandle != null) return;
// Drag
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].DragHandle.ShouldStart(CUI.Input))
{
GrabbedDragHandle = MouseOnList[i].DragHandle;
GrabbedDragHandle.BeginDrag(CUI.Input.MousePosition);
break;
}
if (MouseOnList[i].ConsumeDragAndDrop) break;
}
if (GrabbedDragHandle != null) return;
}
#endregion
#region HandleInput End
#endregion
/// <summary>
/// Obsolete function
/// Will run generator func with this
/// </summary>
/// <param name="initFunc"> Generator function that adds components to passed Main </param>
public void Load(Action<CUIMainComponent> initFunc)
{
RemoveAllChildren();
initFunc(this);
}
public CUIMainComponent() : base()
{
CullChildren = true;
Real = new CUIRect(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
Visible = false;
//IgnoreEvents = true;
ShouldPassPropsToChildren = false;
Debug = true;
ChildrenBoundaries = CUIBoundaries.Box;
}
public void PrintRecalLimitWarning()
{
CUI.Log($"Warning: Your GUI code requires {MaxLayoutRecalcLoopsPerUpdate} layout update loops to fully resolve (which is cringe). Optimize it!", Color.Orange);
}
}
}

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
/// <summary>
/// Swipable and zoomable plane
/// Allows you to place components in a plane
/// and connect them with lines like a graph or scheme
/// </summary>
public class CUIMap : CUIComponent
{
#region CUIMapLink
#endregion
public class CUIMapLink
{
internal static void InitStatic()
{
CUI.OnInit += () => Default = new CUIMapLink(null, null);
CUI.OnDispose += () => Default = null;
}
public static CUIMapLink Default;
public CUIComponent Start;
public CUIComponent End;
//TODO all this crap wasn't designed for nested AKA
public string StartAKA;
public string EndAKA;
public float LineWidth;
public Color LineColor;
public XElement ToXML()
{
XElement connection = new XElement("Connection");
if (LineWidth != Default.LineWidth)
{
connection.SetAttributeValue("LineWidth", LineWidth);
}
connection.SetAttributeValue("Start", StartAKA ?? "");
connection.SetAttributeValue("End", EndAKA ?? "");
return connection;
}
public CUIMapLink(CUIComponent start, CUIComponent end, Color? lineColor = null, float lineWidth = 2f)
{
LineColor = lineColor ?? new Color(128, 128, 128);
LineWidth = lineWidth;
Start = start;
End = end;
StartAKA = start?.AKA;
EndAKA = end?.AKA;
}
}
#region LinksContainer
#endregion
public class LinksContainer : CUIComponent
{
public List<CUIMapLink> Connections = new List<CUIMapLink>();
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch);
foreach (CUIMapLink link in Connections)
{
Vector2 midPoint = new Vector2(link.End.Real.Center.X, link.Start.Real.Center.Y);
GUI.DrawLine(spriteBatch,
link.Start.Real.Center,
midPoint,
link.LineColor, width: link.LineWidth
);
GUI.DrawLine(spriteBatch,
midPoint,
link.End.Real.Center,
link.LineColor, width: link.LineWidth
);
}
}
public LinksContainer()
{
UnCullable = true;
BackgroundColor = Color.Transparent;
Border.Color = Color.Transparent;
}
}
#region CUIMap
#endregion
public LinksContainer linksContainer;
public List<CUIMapLink> Connections => linksContainer.Connections;
public CUIComponent Add(CUIComponent c) => Append(c, c.AKA);
public CUIComponent Connect(CUIComponent startComponent, CUIComponent endComponent, Color? color = null)
{
if (startComponent != null && endComponent != null)
{
if (color == null && (!startComponent.Disabled || !endComponent.Disabled)) color = new Color(0, 0, 255);
linksContainer.Connections.Add(new CUIMapLink(startComponent, endComponent, color));
}
return startComponent;
}
public CUIComponent Connect(CUIComponent startComponent, int end = -2, Color? color = null)
{
end = MathUtils.PositiveModulo(end, Children.Count);
CUIComponent endComponent = Children.ElementAtOrDefault(end);
return Connect(startComponent, endComponent, color);
}
//TODO DRY
public CUIComponent Connect(string start, string end, Color? color = null)
{
CUIComponent startComponent = this[start];
CUIComponent endComponent = this[end];
if (startComponent != null && endComponent != null)
{
if (color == null && (!startComponent.Disabled || !endComponent.Disabled)) color = new Color(0, 0, 255);
linksContainer.Connections.Add(new CUIMapLink(startComponent, endComponent, color)
{
StartAKA = start,
EndAKA = end,
});
}
return startComponent;
}
public CUIComponent Connect(int start, int end, Color? color = null)
{
start = MathUtils.PositiveModulo(start, Children.Count);
end = MathUtils.PositiveModulo(end, Children.Count);
CUIComponent startComponent = Children.ElementAtOrDefault(start);
CUIComponent endComponent = Children.ElementAtOrDefault(end);
return Connect(startComponent, endComponent, color);
}
public CUIComponent ConnectTo(CUIComponent Host, params CUIComponent[] children)
{
foreach (CUIComponent child in children) { Connect(Host, child); }
return Host;
}
public override XElement ToXML(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
Type type = GetType();
XElement element = new XElement(type.Name);
PackProps(element, propAttribute);
XElement connections = new XElement("Connections");
element.Add(connections);
foreach (CUIMapLink link in Connections)
{
connections.Add(link.ToXML());
}
XElement children = new XElement("Children");
element.Add(children);
foreach (CUIComponent child in Children)
{
if (child == linksContainer) continue;
children.Add(child.ToXML());
}
return element;
}
public override void FromXML(XElement element, string baseFolder = null)
{
foreach (XElement childElement in element.Element("Children").Elements())
{
Type childType = CUIReflection.GetComponentTypeByName(childElement.Name.ToString());
if (childType == null) continue;
CUIComponent child = (CUIComponent)Activator.CreateInstance(childType);
child.FromXML(childElement);
this.Append(child, child.AKA);
}
foreach (XElement link in element.Element("Connections").Elements())
{
CUIComponent startComponent = this[link.Attribute("Start").Value];
CUIComponent endComponent = this[link.Attribute("End").Value];
if (startComponent == null || endComponent == null)
{
CUIDebug.Error("startComponent == null || endComponent == null");
continue;
}
Connect(link.Attribute("Start").Value, link.Attribute("End").Value);
}
//TODO: think, this is potentially very bugged,
// Some props might need to be assigned before children, and some after
ExtractProps(element);
}
public CUIMap() : base()
{
Swipeable = true;
ConsumeMouseClicks = true;
CullChildren = true;
BackgroundColor = Color.Transparent;
//without container links won't be culled
//TODO linksContainer should be special and not just first child
this["links"] = linksContainer = new LinksContainer();
OnScroll += (m) =>
{
CUIProps.ChildrenOffset.SetValue(
ChildrenOffset.Zoom(
m.MousePosition - Real.Position,
(-m.Scroll / 500f)
)
);
};
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Button with multiple options
/// which are rotating when you click
/// </summary>
public class CUIMultiButton : CUIButton
{
private List<string> options = new List<string>();
/// <summary>
/// Options are just strings
/// </summary>
[CUISerializable]
public IEnumerable<string> Options
{
get => options;
set => options = value.ToList();
}
public event Action<string> OnSelect;
public Action<string> AddOnSelect { set { OnSelect += value; } }
public bool CycleOnClick { get; set; } = true;
public int SelectedIndex
{
get => options.IndexOf(Selected);
set
{
if (options.Count == 0)
{
Selected = "";
}
else
{
Selected = options.ElementAtOrDefault(value % options.Count) ?? "";
}
}
}
[CUISerializable]
public string Selected
{
get => Text;
set
{
Text = value;
OnSelect?.Invoke(value);
}
}
public void Add(string option) => options.Add(option);
public void Remove(string option)
{
int i = options.IndexOf(option);
options.Remove(option);
if (option == Selected) Select(i);
}
public void Select(int i) => SelectedIndex = i;
public void Select(string option) => Selected = option;
public void SelectNext() => SelectedIndex++;
public void SelectPrev() => SelectedIndex--;
public CUIMultiButton() : base()
{
Text = "MultiButton";
OnMouseDown += (e) =>
{
if (CycleOnClick)
{
SelectNext();
if (Command != null) DispatchUp(new CUICommand(Command, Selected));
}
};
}
/// <summary>
/// CUITextBlock DoWrapFor but for all text
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
protected override Vector2 DoWrapFor(Vector2 size)
{
if ((!WrappedForThisSize.HasValue || size == WrappedForThisSize.Value) && !TextPropChanged) return WrappedSize;
TextPropChanged = false;
WrappedForThisSize = size;
if (Vertical) size = new Vector2(0, size.Y);
IEnumerable<string> WrappedTexts;
if (Wrap || Vertical)
{
WrappedText = Font.WrapText(Text, size.X / TextScale - Padding.X * 2).Trim('\n');
WrappedTexts = options.Select(o => Font.WrapText(o, size.X / TextScale - Padding.X * 2).Trim('\n'));
}
else
{
WrappedText = Text;
WrappedTexts = options;
}
IEnumerable<Vector2> RealTextSizes = WrappedTexts.Select(t => Font.MeasureString(t) * TextScale);
float maxX = 0;
float maxY = 0;
foreach (Vector2 s in RealTextSizes)
{
if (s.X > maxX) maxX = s.X;
if (s.Y > maxY) maxY = s.Y;
}
Vector2 MaxTextSize = new Vector2(maxX, maxY);
RealTextSize = Font.MeasureString(WrappedText) * TextScale;
if (WrappedText == "") RealTextSize = new Vector2(0, 0);
RealTextSize = new Vector2((float)Math.Round(RealTextSize.X), (float)Math.Round(RealTextSize.Y));
Vector2 minSize = MaxTextSize + Padding * 2;
if (!Wrap || Vertical)
{
SetForcedMinSize(new CUINullVector2(minSize));
}
WrappedSize = new Vector2(Math.Max(size.X, minSize.X), Math.Max(size.Y, minSize.Y));
return WrappedSize;
}
internal override Vector2 AmIOkWithThisSize(Vector2 size)
{
return DoWrapFor(size);
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Container for other components
/// Can have only 1 child
/// Sets component as it's only child when you open it (as a page)
/// </summary>
public class CUIPages : CUIComponent
{
public CUIComponent OpenedPage;
public bool IsOpened(CUIComponent p) => OpenedPage == p;
/// <summary>
/// Adds page as its only child
/// </summary>
/// <param name="page"></param>
public void Open(CUIComponent page)
{
RemoveAllChildren();
Append(page);
page.Relative = new CUINullRect(0, 0, 1, 1);
OpenedPage = page;
}
public CUIPages() : base()
{
BackgroundColor = Color.Transparent;
Border.Color = Color.Transparent;
CullChildren = false;
}
}
}

View File

@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
namespace QICrabUI
{
/// <summary>
/// Passive block of text
/// </summary>
public class CUITextBlock : CUIComponent
{
public event Action OnTextChanged;
public Action AddOnTextChanged { set { OnTextChanged += value; } }
//TODO move padding here, it makes no sense in CUIComponent
private bool wrap;
[CUISerializable]
public bool Wrap
{
get => wrap;
set
{
wrap = value;
MeasureUnwrapped();
TextPropChanged = true;
}
}
[CUISerializable] public Color TextColor { get; set; }
private GUIFont font = GUIStyle.Font;
[CUISerializable]
public GUIFont Font
{
get => font;
set
{
font = value;
MeasureUnwrapped();
TextPropChanged = true;
}
}
/// <summary>
/// A Vector2 ([0..1],[0..1])
/// </summary>
[CUISerializable] public Vector2 TextAlign { get; set; }
[CUISerializable] public bool Vertical { get; set; }
/// <summary>
/// Lil optimization: ghost text won't set forsed size and parent won't be able to fit to it
/// But it will increase performance in large lists
/// </summary>
[CUISerializable] public bool GhostText { get; set; }
[CUISerializable]
public string Text { get => text; set => SetText(value); }
[CUISerializable]
public float TextScale { get => textScale; set => SetTextScale(value); }
#region Cringe
protected Vector2 RealTextSize;
[Calculated] protected Vector2 TextDrawPos { get; set; }
[Calculated] protected string WrappedText { get; set; } = "";
protected Vector2? WrappedForThisSize;
[Calculated] protected Vector2 WrappedSize { get; set; }
public Vector2 UnwrappedTextSize { get; set; }
public Vector2 UnwrappedMinSize { get; set; }
protected bool TextPropChanged;
#endregion
protected string text = ""; internal void SetText(string value)
{
text = value ?? "";
OnTextChanged?.Invoke();
MeasureUnwrapped();
TextPropChanged = true;
OnPropChanged();
OnAbsolutePropChanged();
}
protected float textScale = 0.9f; internal void SetTextScale(float value)
{
textScale = value;
MeasureUnwrapped();
TextPropChanged = true;
OnPropChanged();
OnAbsolutePropChanged();
}
//Note: works only on unwrapped text for now because WrappedText is delayed
/// <summary>
/// X coordinate of caret if there was one
/// Used in CUITextInput, you don't need this
/// </summary>
/// <param name="i"></param>
/// <returns></returns>
public float CaretPos(int i)
{
return Font.MeasureString(Text.SubstringSafe(0, i)).X * TextScale + Padding.X;
}
//Note: works only on unwrapped text for now because WrappedText is delayed
/// <summary>
/// Tndex of caret if there was one
/// Used in CUITextInput, you don't need this
/// </summary>
/// <param name="i"></param>
/// <returns></returns>
public int CaretIndex(float x)
{
int Aprox = (int)Math.Round((x - Padding.X) / Font.MeasureString(Text).X * Text.Length);
int closestCaretPos = Aprox;
float smallestDif = Math.Abs(x - CaretPos(Aprox));
for (int i = Aprox - 2; i <= Aprox + 2; i++)
{
float dif = Math.Abs(x - CaretPos(i));
if (dif < smallestDif)
{
closestCaretPos = i;
smallestDif = dif;
}
}
return closestCaretPos;
}
// Small optimisation, doesn't seem to save much
protected virtual void MeasureUnwrapped()
{
UnwrappedTextSize = Font.MeasureString(Text) * TextScale;
UnwrappedMinSize = UnwrappedTextSize + Padding * 2;
}
protected virtual Vector2 DoWrapFor(Vector2 size)
{
// To prevent loop
if (!(WrappedForThisSize.HasValue && WrappedForThisSize != size) && !TextPropChanged) return WrappedSize;
TextPropChanged = false;
WrappedForThisSize = size;
// There's no way to wrap vertical text
bool isInWrapZone = Vertical ? false : size.X <= UnwrappedMinSize.X;
bool isSolid = Vertical || !Wrap;
if (Vertical) size = new Vector2(0, size.Y);
if ((Wrap && isInWrapZone) || Vertical)
{
WrappedText = Font.WrapText(Text, size.X / TextScale - Padding.X * 2).Trim('\n');
RealTextSize = Font.MeasureString(WrappedText) * TextScale;
}
else
{
WrappedText = Text;
RealTextSize = UnwrappedTextSize;
}
if (WrappedText == "") RealTextSize = new Vector2(0, 0);
RealTextSize = new Vector2((float)Math.Round(RealTextSize.X), (float)Math.Round(RealTextSize.Y));
Vector2 minSize = RealTextSize + Padding * 2;
if (isSolid && !GhostText)
{
SetForcedMinSize(new CUINullVector2(minSize));
}
WrappedSize = new Vector2(Math.Max(size.X, minSize.X), Math.Max(size.Y, minSize.Y));
return WrappedSize;
}
internal override Vector2 AmIOkWithThisSize(Vector2 size)
{
return DoWrapFor(size);
}
//Note: This is a bottleneck for large lists of text
internal override void UpdatePseudoChildren()
{
if (CulledOut) return;
TextDrawPos = CUIAnchor.GetChildPos(Real, TextAlign, Vector2.Zero, RealTextSize / Scale) + Padding * CUIAnchor.Direction(TextAlign) / Scale;
//CUIDebug.Capture(null, this, "UpdatePseudoChildren", "", "TextDrawPos", $"{TextDrawPos - Real.Position}");
}
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch);
// Font.DrawString(spriteBatch, WrappedText, TextDrawPos, TextColor, rotation: 0, origin: Vector2.Zero, TextScale, spriteEffects: SpriteEffects.None, layerDepth: 0.1f);
Font.Value.DrawString(spriteBatch, WrappedText, TextDrawPos, TextColor, rotation: 0, origin: Vector2.Zero, TextScale / Scale, se: SpriteEffects.None, layerDepth: 0.1f);
}
public CUITextBlock() { }
public CUITextBlock(string text) : this()
{
Text = text;
}
}
}

View File

@@ -0,0 +1,479 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using EventInput;
using System.Windows;
namespace QICrabUI
{
/// <summary>
/// Text input
/// </summary>
public class CUITextInput : CUIComponent, IKeyboardSubscriber
{
#region IKeyboardSubscriber
private Keys pressedKey;
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveSpecialInput(Keys key)
{
try
{
pressedKey = key;
if (key == Keys.Back) Back();
if (key == Keys.Delete) Delete();
if (key == Keys.Left) MoveLeft();
if (key == Keys.Right) MoveRight();
}
catch (Exception e)
{
CUI.Warning(e);
}
}
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveTextInput(char inputChar) => ReceiveTextInput(inputChar.ToString());
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveTextInput(string text)
{
try
{
CutSelection();
Text = Text.Insert(CaretPos, text);
CaretPos = CaretPos + 1;
OnTextAdded?.Invoke(text);
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
//CUI.Log($"ReceiveTextInput {text}");
}
catch (Exception e)
{
CUI.Log(e);
}
}
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveCommandInput(char command)
{
try
{
if (pressedKey == Keys.A) SelectAll();
if (pressedKey == Keys.C) Copy();
if (pressedKey == Keys.V) Paste();
}
catch (Exception e)
{
CUI.Warning(e);
}
//CUI.Log($"ReceiveCommandInput {command}");
}
//Alt+tab?
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveEditingInput(string text, int start, int length)
{
//CUI.Log($"ReceiveEditingInput {text} {start} {length}");
}
//TODO mb should lose focus here
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public bool Selected { get; set; }
#endregion
#region Commands
public void SelectAll() => Select(0, Text.Length);
public void Copy()
{
if (Selection.IsEmpty) return;
selectionHandle.Grabbed = false;
Clipboard.SetText(Text.SubstringSafe(Selection.Start, Selection.End));
}
public void Paste()
{
ReceiveTextInput(Clipboard.GetText());
}
public void AddText(string text) => ReceiveTextInput(text);
public void MoveLeft()
{
CaretPos--;
Selection = IntRange.Zero;
}
public void MoveRight()
{
CaretPos++;
Selection = IntRange.Zero;
}
// //TODO
// public void SelectLeft()
// {
// if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos - 1, CaretPos);
// else Selection = new IntRange(Selection.Start - 1, Selection.End);
// }
// //TODO
// public void SelectRight()
// {
// if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos, CaretPos + 1);
// else Selection = new IntRange(Selection.Start, Selection.End + 1);
// }
public void Back()
{
TextInputState oldState = State;
if (!Selection.IsEmpty) CutSelection();
else
{
string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos - 1);
string s2 = oldState.Text.SubstringSafe(oldState.CaretPos);
Text = s1 + s2;
CaretPos = oldState.CaretPos - 1;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
}
}
public void Delete()
{
TextInputState oldState = State;
if (!Selection.IsEmpty) CutSelection();
else
{
string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos);
string s2 = oldState.Text.SubstringSafe(oldState.CaretPos + 1);
Text = s1 + s2;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
//CaretPos = oldState.CaretPos;
}
}
public void CutSelection()
{
if (Selection.IsEmpty) return;
selectionHandle.Grabbed = false;
string s1 = Text.SubstringSafe(0, Selection.Start);
string s2 = Text.SubstringSafe(Selection.End);
Text = s1 + s2;
CaretPos = Selection.Start;
Selection = IntRange.Zero;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
}
internal int SetCaretPos(Vector2 v)
{
int newCaretPos = TextComponent.CaretIndex(v.X);
CaretPos = newCaretPos;
return newCaretPos;
}
#endregion
internal class SelectionHandle
{
public bool Grabbed;
public int lastSelectedPos;
}
internal SelectionHandle selectionHandle = new SelectionHandle();
internal record struct TextInputState(string Text, IntRange Selection, int CaretPos)
{
public string Text { get; init; } = Text ?? "";
}
private TextInputState state; internal TextInputState State
{
get => state;
set
{
state = ValidateState(value);
ApplyState(state);
}
}
internal TextInputState ValidateState(TextInputState state)
{
//return state with { CaretPos = state.CaretPos.Fit(0, state.Text.Length - 1) };
string newText = state.Text;
IntRange newSelection = new IntRange(
state.Selection.Start.Fit(0, newText.Length),
state.Selection.End.Fit(0, newText.Length)
);
int newCaretPos = state.CaretPos.Fit(0, newText.Length);
return new TextInputState(newText, newSelection, newCaretPos);
}
internal void ApplyState(TextInputState state)
{
TextComponent.Text = state.Text;
SelectionOverlay.Visible = !state.Selection.IsEmpty;
CaretIndicatorVisible = Focused && !SelectionOverlay.Visible;
if (!state.Selection.IsEmpty)
{
SelectionOverlay.Absolute = SelectionOverlay.Absolute with
{
Left = TextComponent.CaretPos(state.Selection.Start),
Width = TextComponent.CaretPos(state.Selection.End) - TextComponent.CaretPos(state.Selection.Start),
};
}
CaretIndicator.Absolute = CaretIndicator.Absolute with
{
Left = TextComponent.CaretPos(state.CaretPos),
};
}
private bool valid = true; public bool Valid
{
get => valid;
set
{
if (valid == value) return;
valid = value;
UpdateBorderColor();
}
}
public Type VatidationType { get; set; }
public bool IsValidText(string text)
{
if (VatidationType == null) return true;
if (VatidationType == typeof(string)) return true;
if (VatidationType == typeof(Color)) return true;
if (VatidationType == typeof(bool)) return bool.TryParse(text, out _);
if (VatidationType == typeof(int)) return int.TryParse(text, out _);
if (VatidationType == typeof(float)) return float.TryParse(text, out _);
if (VatidationType == typeof(double)) return double.TryParse(text, out _);
return false;
}
//TODO this is cringe
// public override void Consume(object o)
// {
// string value = (string)o;
// State = new TextInputState(value, State.Selection, State.CaretPos);
// Valid = IsValidText(value);
// }
internal CUITextBlock TextComponent;
public string Text
{
get => State.Text;
set
{
if (Disabled) return;
State = new TextInputState(value, State.Selection, State.CaretPos);
bool isvalid = IsValidText(value);
if (isvalid)
{
OnTextChanged?.Invoke(State.Text);
}
Valid = isvalid;
}
}
internal CUIComponent SelectionOverlay;
public IntRange Selection
{
get => State.Selection;
set => State = new TextInputState(State.Text, value, State.CaretPos);
}
public void Select(int start, int end) => Selection = new IntRange(start, end);
public bool CaretIndicatorVisible { get; set; }
public double CaretBlinkInterval { get; set; } = 0.5;
internal CUIComponent CaretIndicator;
public int CaretPos
{
get => State.CaretPos;
set => State = new TextInputState(State.Text, State.Selection, value);
}
//TODO
//[CUISerializable] public bool PreventOverflow { get; set; } = false;
public void UpdateBorderColor()
{
if (Valid)
{
if (Focused)
{
Style["Border"] = "CUIPalette.Input.Focused";
}
else
{
Style["Border"] = "CUIPalette.Input.Border";
}
}
else
{
Style["Border"] = "CUIPalette.Input.Invalid";
}
}
[CUISerializable]
public float TextScale
{
get => TextComponent?.TextScale ?? 0;
set => TextComponent.TextScale = value;
}
public Color TextColor
{
get => TextComponent?.TextColor ?? Color.White;
set
{
if (TextComponent != null)
{
TextComponent.TextColor = value;
}
}
}
public event Action<string> OnTextChanged;
public Action<string> AddOnTextChanged { set => OnTextChanged += value; }
public event Action<string> OnTextAdded;
public Action<string> AddOnTextAdded { set => OnTextAdded += value; }
public override void Draw(SpriteBatch spriteBatch)
{
if (Focused)
{
CaretIndicator.Visible = CaretIndicatorVisible && Timing.TotalTime % CaretBlinkInterval > CaretBlinkInterval / 2;
}
base.Draw(spriteBatch);
}
public CUITextInput(string text) : this()
{
Text = text;
}
public CUITextInput() : base()
{
AbsoluteMin = new CUINullRect(w: 50, h: 22);
FitContent = new CUIBool2(true, true);
Focusable = true;
Border.Thickness = 2;
HideChildrenOutsideFrame = true;
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
BreakSerialization = true;
this["TextComponent"] = TextComponent = new CUITextBlock()
{
Text = "",
Relative = new CUINullRect(0, 0, 1, 1),
TextAlign = CUIAnchor.CenterLeft,
Style = new CUIStyle(){
{"Padding", "[2,2]"},
{"TextColor", "CUIPalette.Input.Text"},
},
};
this["SelectionOverlay"] = SelectionOverlay = new CUIComponent()
{
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.Input.Selection"},
{"Border", "Transparent"},
},
Relative = new CUINullRect(h: 1),
Ghost = new CUIBool2(true, true),
IgnoreParentVisibility = true,
};
this["CaretIndicator"] = CaretIndicator = new CUIComponent()
{
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.Input.Caret"},
{"Border", "Transparent"},
},
Relative = new CUINullRect(y: 0.1f, h: 0.8f),
Absolute = new CUINullRect(w: 1),
Ghost = new CUIBool2(true, true),
Visible = false,
IgnoreParentVisibility = true,
};
OnConsume += (o) =>
{
string value = o.ToString();
State = new TextInputState(value, State.Selection, State.CaretPos);
Valid = IsValidText(value);
};
OnFocus += () =>
{
UpdateBorderColor();
CaretIndicator.Visible = true;
};
OnFocusLost += () =>
{
UpdateBorderColor();
Selection = IntRange.Zero;
CaretIndicator.Visible = false;
};
OnMouseDown += (e) =>
{
int newCaretPos = SetCaretPos(e.MousePosition - Real.Position);
Selection = IntRange.Zero;
selectionHandle.lastSelectedPos = newCaretPos;
selectionHandle.Grabbed = true;
};
OnMouseMove += (e) =>
{
if (selectionHandle.Grabbed)
{
int nextCaretPos = SetCaretPos(e.MousePosition - Real.Position);
Selection = new IntRange(selectionHandle.lastSelectedPos, nextCaretPos);
}
};
OnDClick += (e) => SelectAll();
if (CUI.Main is not null)
{
CUI.Main.Global.OnMouseUp += (e) => selectionHandle.Grabbed = false;
}
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Extensions;
namespace QICrabUI
{
/// <summary>
/// A button which can be on and off
/// It's derived from CUITextBlock and has all its props
/// </summary>
public class CUIToggleButton : CUITextBlock
{
[CUISerializable]
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
[CUISerializable] public Color DisabledColor { get; set; }
[CUISerializable] public Color OnColor { get; set; }
[CUISerializable] public Color OnHoverColor { get; set; }
[CUISerializable] public Color OffColor { get; set; }
[CUISerializable] public Color OffHoverColor { get; set; }
public Color MasterColor
{
set
{
OffColor = value.Multiply(0.5f);
OffHoverColor = value;
OnColor = value.Multiply(0.9f);
OnHoverColor = value;
}
}
public Color MasterColorOpaque
{
set
{
OffColor = new Color((int)(value.R * 0.5f), (int)(value.G * 0.5f), (int)(value.B * 0.5f), value.A);
OffHoverColor = value;
OnColor = new Color((int)(value.R * 0.9f), (int)(value.G * 0.9f), (int)(value.B * 0.9f), value.A); ;
OnHoverColor = value;
}
}
// BackgroundColor is used in base.Draw, but here it's calculated from OnColor/OffColor
// so it's not a prop anymore, and i don't want to serialize it
public new Color BackgroundColor { get => CUIProps.BackgroundColor.Value; set => CUIProps.BackgroundColor.SetValue(value); }
private string onText;
private string offText;
[CUISerializable]
public string OnText
{
get => onText;
set { onText = value; if (State && onText != null) Text = onText; }
}
[CUISerializable]
public string OffText
{
get => offText;
set { offText = value; if (!State && offText != null) Text = offText; }
}
public event Action<bool> OnStateChange;
public Action<bool> AddOnStateChange { set { OnStateChange += value; } }
protected bool state;
[CUISerializable]
public bool State
{
get => state;
set
{
state = value;
if (state && OnText != null) Text = OnText;
if (!state && OffText != null) Text = OffText;
}
}
public override void Draw(SpriteBatch spriteBatch)
{
if (Disabled)
{
BackgroundColor = DisabledColor;
}
else
{
if (State)
{
if (MouseOver) BackgroundColor = OnHoverColor;
else BackgroundColor = OnColor;
}
else
{
if (MouseOver) BackgroundColor = OffHoverColor;
else BackgroundColor = OffColor;
}
}
base.Draw(spriteBatch);
}
public CUIToggleButton() : base()
{
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
//BackgroundColor = OffColor;
TextAlign = new Vector2(0.5f, 0.5f);
Padding = new Vector2(4, 2);
Text = nameof(CUIToggleButton);
OnMouseDown += (e) =>
{
if (!Disabled)
{
State = !State;
SoundPlayer.PlayUISound(ClickSound);
OnStateChange?.Invoke(State);
}
};
}
public CUIToggleButton(string text) : this()
{
Text = text;
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Resizing components to it's Width and placing them sequentially
/// </summary>
public class CUIVerticalList : CUIComponent
{
[CUISerializable] public bool Scrollable { get; set; }
[CUISerializable] public float ScrollSpeed { get; set; } = 1.0f;
[CUISerializable] public float TopGap { get; set; } = 0;
[CUISerializable] public float BottomGap { get; set; } = 10f;
public override CUILayout Layout
{
get => layout;
set
{
layout = new CUILayoutVerticalList();
layout.Host = this;
}
}
public CUILayoutVerticalList ListLayout => (CUILayoutVerticalList)Layout;
[CUISerializable]
public CUIDirection Direction
{
get => ListLayout.Direction;
set => ListLayout.Direction = value;
}
//TODO test, sync with hlist
[CUISerializable]
public float Gap
{
get => ListLayout.Gap;
set => ListLayout.Gap = value;
}
[CUISerializable]
public bool ResizeToHostWidth
{
get => ListLayout.ResizeToHostWidth;
set => ListLayout.ResizeToHostWidth = value;
}
public float Scroll
{
get => ChildrenOffset.Y;
set
{
if (!Scrollable) return;
CUIProps.ChildrenOffset.SetValue(
ChildrenOffset with { Y = value }
);
}
}
internal override CUIBoundaries ChildOffsetBounds => new CUIBoundaries(
minX: 0,
maxX: 0,
maxY: TopGap,
minY: Math.Min(Real.Height - ListLayout.TotalHeight - BottomGap, 0)
);
public CUIVerticalList() : base()
{
CullChildren = true;
OnScroll += (m) => Scroll += m.Scroll * ScrollSpeed;
ChildrenBoundaries = CUIBoundaries.VerticalTube;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Simple dialog box with a message and ok button
/// use public static void Open(string msg) to open it
/// </summary>
public class CUIMessageBox : CUIFrame
{
public static void Open(string msg)
{
CUI.TopMain.Append(new CUIMessageBox(msg));
}
public CUIMessageBox(string msg) : base()
{
Palette = PaletteOrder.Quaternary;
Resizible = false;
Relative = new CUINullRect(0, 0, 0.25f, 0.25f);
Anchor = CUIAnchor.Center;
OutlineThickness = 2;
this["layout"] = new CUIVerticalList()
{
Relative = new CUINullRect(0, 0, 1, 1),
};
this["layout"]["main"] = new CUIVerticalList()
{
FillEmptySpace = new CUIBool2(false, true),
Scrollable = true,
ScrollSpeed = 0.5f,
Style = CUIStylePrefab.Main,
};
this["layout"]["main"]["msg"] = new CUITextBlock(msg)
{
TextScale = 1.2f,
Wrap = true,
Font = GUIStyle.Font,
TextAlign = CUIAnchor.TopCenter,
Style = new CUIStyle()
{
["Padding"] = "[10,10]",
},
};
this["layout"]["ok"] = new CUIButton("Ok")
{
TextScale = 1.0f,
Style = new CUIStyle()
{
["Padding"] = "[10,10]",
},
AddOnMouseDown = (e) => this.RemoveSelf(),
};
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace QICrabUI
{
// hmm, idk if this should be a prefab or component
// it's too small for component
// but in prefab i can't use initializer
public class CUICloseButton : CUIButton
{
public CUICloseButton() : base()
{
Command = "Close";
Text = "";
ZIndex = 10;
BackgroundSprite = CUI.TextureManager.GetCUISprite(3, 1);
Absolute = new CUINullRect(0, 0, 15, 15);
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// It's a debug tool, you can use it with cuimg command, it's very fps comsuming
/// </summary>
[NoDefault]
public class CUIMagnifyingGlass : CUICanvas
{
public static CUIFrame GlassFrame;
public static void AddToggleButton()
{
CUI.TopMain["ToggleMagnifyingGlass"] = new CUIButton("MG")
{
Absolute = new CUINullRect(0, 0, 20, 20),
Anchor = CUIAnchor.CenterLeft,
AddOnMouseDown = (e) => ToggleEquip(),
};
}
public static void ToggleEquip()
{
if (GlassFrame != null)
{
GlassFrame.RemoveSelf();
GlassFrame = null;
}
else
{
GlassFrame = new CUIFrame()
{
ZIndex = 100000,
BackgroundColor = Color.Transparent,
Border = new CUIBorder(Color.Cyan, 5),
Anchor = CUIAnchor.Center,
Absolute = new CUINullRect(w: 200, h: 200),
};
GlassFrame["glass"] = new CUIMagnifyingGlass();
CUI.TopMain["MagnifyingGlass"] = GlassFrame;
}
}
public override void CleanUp()
{
texture.Dispose();
base.CleanUp();
}
Texture2D texture;
Color[] backBuffer;
double lastDrawn;
public override void Draw(SpriteBatch spriteBatch)
{
if (Timing.TotalTime - lastDrawn > 0.05)
{
lastDrawn = Timing.TotalTime;
GameMain.Instance.GraphicsDevice.GetBackBufferData<Color>(backBuffer);
texture.SetData(backBuffer);
texture.GetData<Color>(
0, new Rectangle((int)Real.Left, (int)Real.Top, 40, 40), Data, 0, Data.Length
);
SetData();
}
base.Draw(spriteBatch);
}
public CUIMagnifyingGlass() : base()
{
Size = new Point(40, 40);
SamplerState = CUI.NoSmoothing;
Relative = new CUINullRect(0, 0, 1, 1);
int w = GameMain.Instance.GraphicsDevice.PresentationParameters.BackBufferWidth;
int h = GameMain.Instance.GraphicsDevice.PresentationParameters.BackBufferHeight;
backBuffer = new Color[w * h];
texture = new Texture2D(GameMain.Instance.GraphicsDevice, w, h, false, GameMain.Instance.GraphicsDevice.PresentationParameters.BackBufferFormat);
}
}
}

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using EventInput;
using System.Windows;
namespace QICrabUI
{
//TODO move all this to defauld styles
/// <summary>
/// CUITextBlock adapted for CUIMenu
/// </summary>
public class CUIMenuText : CUITextBlock
{
public CUIMenuText(string text) : this() => Text = text;
public CUIMenuText() : base()
{
Anchor = CUIAnchor.Center;
TextScale = 1.0f;
ZIndex = 100;
TextColor = Color.Black;
}
}
/// <summary>
/// Component with a sprite that will notify parent CUIMenu when clicked
/// </summary>
public class CUIMenuOption : CUIComponent
{
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
/// <summary>
/// This is the Value that will be send to CUIMenu on click, and will be passed to OnSelect event
/// </summary>
[CUISerializable] public string Value { get; set; }
/// <summary>
/// Normal background color
/// </summary>
[CUISerializable]
public Color BaseColor
{
get => (Color)Animations["hover"].StartValue;
set
{
Animations["hover"].StartValue = value;
Animations["hover"].ApplyValue();
}
}
/// <summary>
/// Background color when hovered
/// </summary>
[CUISerializable]
public Color HoverColor
{
get => (Color)Animations["hover"].EndValue;
set => Animations["hover"].EndValue = value;
}
public CUIMenuOption()
{
BackgroundColor = new Color(255, 255, 255, 255);
Relative = new CUINullRect(0, 0, 1, 1);
IgnoreTransparent = true;
Command = "CUIMenuOption select";
OnMouseDown += (e) =>
{
SoundPlayer.PlayUISound(ClickSound);
DispatchUp(new CUICommand(Command, Value));
};
Animations["hover"] = new CUIAnimation()
{
StartValue = new Color(255, 255, 255, 255),
EndValue = new Color(255, 255, 255, 255),
Duration = 0.1,
ReverseDuration = 0.3,
Property = "BackgroundColor",
};
OnMouseEnter += (e) => Animations["hover"].Forward();
OnMouseLeave += (e) => Animations["hover"].Back();
}
}
public class CUIMenu : CUIComponent, IKeyboardSubscriber
{
// this allows it to intercept esc key press naturally,
// but it also blocks normal hotkey bindings, so idk
// ----------------- IKeyboardSubscriber -----------------
public void ReceiveSpecialInput(Keys key) { if (key == Keys.Escape) Close(); }
public void ReceiveTextInput(char inputChar) => ReceiveTextInput(inputChar.ToString());
public void ReceiveTextInput(string text) { }
public void ReceiveCommandInput(char command) { }
public void ReceiveEditingInput(string text, int start, int length) { }
public bool Selected { get; set; }
// ----------------- IKeyboardSubscriber -----------------
public static void InitStatic() => CUI.OnDispose += () => Menus.Clear();
/// <summary>
/// All loaded menus are stored here by Name
/// </summary>
public static Dictionary<string, CUIMenu> Menus = new();
/// <summary>
/// Initial fade in animation duration, set to 0 to disable
/// </summary>
[CUISerializable]
public double FadeInDuration
{
get => Animations["fade"].Duration;
set => Animations["fade"].Duration = value;
}
/// <summary>
/// Will be used as key for this menu in CUIMenu.Menus
/// </summary>
[CUISerializable] public string Name { get; set; }
/// <summary>
/// Does nothing, just a prop so you could get author programmatically
/// </summary>
[CUISerializable] public string Author { get; set; }
/// <summary>
/// If true will act as IKeyboardSubscriber. Don't
/// </summary>
[CUISerializable] public bool BlockInput { get; set; }
/// <summary>
/// Happens when some CUIMenuOption is clicked, the value of that option is passed to it
/// </summary>
public event Action<string> OnSelect;
/// <summary>
/// Will add this as a child to host (CUI.Main) and start fadein animation
/// </summary>
public void Open(CUIComponent host = null)
{
if (Parent != null) return;
host ??= CUI.Main;
host.Append(this);
if (BlockInput) CUI.FocusedComponent = this;
Animations["fade"].SetToStart();
Animations["fade"].Forward();
}
public void Close() => RemoveSelf();
public void Toggle(CUIComponent host = null)
{
if (Parent != null) Close();
else Open(host);
}
/// <summary>
/// Loads CUIMenu from a file to CUIMenu.Menus
/// </summary>
public static CUIMenu Load(string path)
{
CUIMenu menu = CUIComponent.LoadFromFile<CUIMenu>(path);
if (menu == null) CUI.Warning($"Couldn't load CUIMenu from {path}");
if (menu?.Name != null) Menus[menu.Name] = menu;
return menu;
}
public CUIMenu() : base()
{
BackgroundColor = new Color(255, 255, 255, 255);
Anchor = CUIAnchor.Center;
Transparency = 0.0f;
AddCommand("CUIMenuOption select", (o) =>
{
if (o is string s) OnSelect?.Invoke(s);
//Close();
});
Animations["fade"] = new CUIAnimation()
{
StartValue = 0.0f,
EndValue = 1.0f,
Duration = 0.2,
Property = "Transparency",
};
if (CUI.Main != null)
{
CUI.Main.Global.OnKeyDown += (e) =>
{
if (e.PressedKeys.Contains(Keys.Escape)) Close();
};
CUI.Main.OnMouseDown += (e) => Close();
}
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Unfinished crap, don't use
/// </summary>
public class CUIRadialMenuOption : CUIComponent
{
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
public Color BaseColor
{
get => (Color)this.Animations["hover"].StartValue;
set => this.Animations["hover"].StartValue = value;
}
public Color Hover
{
get => (Color)this.Animations["hover"].EndValue;
set => this.Animations["hover"].EndValue = value;
}
public float TextRadius { get; set; } = 0.4f;
public void SetRotation(float angle)
{
BackgroundSprite.Offset = new Vector2(0.5f, 0.5f);
BackgroundSprite.Rotation = angle;
this["Text"].Relative = new CUINullRect(
(float)(TextRadius * Math.Cos(angle - Math.PI / 2)),
(float)(TextRadius * Math.Sin(angle - Math.PI / 2))
);
}
public Action Callback;
public CUIRadialMenuOption(string name = "", Action callback = null)
{
IgnoreTransparent = true;
Relative = new CUINullRect(0, 0, 1, 1);
Callback = callback;
OnMouseDown += (e) =>
{
SoundPlayer.PlayUISound(ClickSound);
Callback?.Invoke();
};
this.Animations["hover"] = new CUIAnimation()
{
StartValue = new Color(255, 255, 255, 255),
EndValue = new Color(0, 255, 255, 255),
Property = "BackgroundColor",
Duration = 0.2,
ReverseDuration = 0.3,
};
this.Animations["hover"].ApplyValue();
OnMouseEnter += (e) => Animations["hover"].Forward();
OnMouseLeave += (e) => Animations["hover"].Back();
this["Text"] = new CUITextBlock(name)
{
Anchor = CUIAnchor.Center,
ZIndex = 100,
TextScale = 1.0f,
};
}
}
/// <summary>
/// Unfinished crap, don't use
/// </summary>
public class CUIRadialMenu : CUIComponent
{
public CUIRadialMenuOption OptionTemplate = new();
public Dictionary<string, CUIRadialMenuOption> Options = new();
public CUIRadialMenuOption AddOption(string name, Action callback)
{
CUIRadialMenuOption option = new CUIRadialMenuOption(name, callback);
option.ApplyState(OptionTemplate);
option.Animations["hover"].Interpolate = OptionTemplate.Animations["hover"].Interpolate;
option.Animations["hover"].ApplyValue();
this[name] = Options[name] = option;
option.OnClick += (e) => Close();
CalculateRotations();
return option;
}
public void CalculateRotations()
{
float delta = (float)(Math.PI * 2 / Options.Count);
int i = 0;
foreach (CUIRadialMenuOption option in Options.Values)
{
option.SetRotation(delta * i);
i++;
}
}
public bool IsOpened => Parent != null;
public void Open(CUIComponent host = null)
{
host ??= CUI.Main;
host.Append(this);
Animations["fade"].SetToStart();
Animations["fade"].Forward();
}
public void Close()
{
// BlockChildrenAnimations();
// Animations["fade"].SetToEnd();
// Animations["fade"].Back();
RemoveSelf();
}
public CUIRadialMenu() : base()
{
Anchor = CUIAnchor.Center;
Relative = new CUINullRect(h: 0.8f);
CrossRelative = new CUINullRect(w: 0.8f);
BackgroundColor = new Color(255, 255, 255, 255);
//BackgroundSprite = new CUISprite("RadialMenu.png");
Animations["fade"] = new CUIAnimation()
{
StartValue = 0.0f,
EndValue = 1.0f,
Property = "Transparency",
Duration = 0.2,
ReverseDuration = 0.1,
};
//HACK
Animations["fade"].OnStop += (dir) =>
{
if (dir == CUIDirection.Reverse)
{
RemoveSelf();
}
};
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Horizontal range input
/// </summary>
public class CUISlider : CUIComponent
{
/// <summary>
/// Happens when handle is dragged, value is [0..1]
/// </summary>
public event Action<float> OnSlide;
public Action<float> AddOnSlide { set { OnSlide += value; } }
public float InOutMult => (Real.Width - Real.Height) / Real.Width;
private float lambda;
private float? pendingLambda;
public float Lambda
{
get => lambda;
set
{
lambda = Math.Clamp(value, 0, 1);
pendingLambda = lambda;
}
}
[CUISerializable] public FloatRange Range { get; set; } = new FloatRange(0, 1);
[CUISerializable] public int? Precision { get; set; } = 2;
/// <summary>
/// The handle
/// </summary>
public CUIComponent Handle;
public CUIComponent LeftEnding;
public CUIComponent RightEnding;
public CUISprite MiddleSprite;
public CUIRect MiddleRect;
public Color MasterColor
{
set
{
if (LeftEnding != null) LeftEnding.BackgroundColor = value;
if (RightEnding != null) RightEnding.BackgroundColor = value;
if (Handle != null) Handle.BackgroundColor = value;
}
}
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch);
CUI.DrawRectangle(spriteBatch, MiddleRect, LeftEnding.BackgroundColor, MiddleSprite);
}
public CUISlider() : base()
{
ChildrenBoundaries = CUIBoundaries.Box;
BreakSerialization = true;
this["LeftEnding"] = LeftEnding = new CUIComponent()
{
Anchor = CUIAnchor.CenterLeft,
Relative = new CUINullRect(h: 1),
CrossRelative = new CUINullRect(w: 1),
BackgroundSprite = CUI.TextureManager.GetCUISprite(2, 2, CUISpriteDrawMode.Resize, SpriteEffects.FlipHorizontally),
Style = new CUIStyle()
{
["Border"] = "Transparent",
["BackgroundColor"] = "CUIPalette.Slider",
},
};
this["RightEnding"] = RightEnding = new CUIComponent()
{
Anchor = CUIAnchor.CenterRight,
Relative = new CUINullRect(h: 1),
CrossRelative = new CUINullRect(w: 1),
BackgroundSprite = CUI.TextureManager.GetCUISprite(2, 2),
Style = new CUIStyle()
{
["Border"] = "Transparent",
["BackgroundColor"] = "CUIPalette.Slider",
},
};
this["handle"] = Handle = new CUIComponent()
{
Style = new CUIStyle()
{
["Border"] = "Transparent",
["BackgroundColor"] = "CUIPalette.Slider",
},
Draggable = true,
BackgroundSprite = CUI.TextureManager.GetCUISprite(0, 2),
Relative = new CUINullRect(h: 1),
CrossRelative = new CUINullRect(w: 1),
AddOnDrag = (x, y) =>
{
lambda = Math.Clamp(x / InOutMult, 0, 1);
OnSlide?.Invoke(lambda);
if (Command != null)
{
float value = Range.PosOf(lambda);
if (Precision.HasValue) value = (float)Math.Round(value, Precision.Value);
DispatchUp(new CUICommand(Command, value));
}
},
};
Handle.DragHandle.DragRelative = true;
MiddleSprite = CUI.TextureManager.GetSprite("CUI.png", new Rectangle(44, 64, 6, 32));
OnLayoutUpdated += () =>
{
MiddleRect = new CUIRect(
Real.Left + Real.Height,
Real.Top,
Real.Width - 2 * Real.Height,
Real.Height
);
if (pendingLambda.HasValue)
{
Handle.Relative = Handle.Relative with
{
Left = Math.Clamp(pendingLambda.Value, 0, 1) * InOutMult,
};
pendingLambda = null;
}
};
OnConsume += (o) =>
{
if (float.TryParse(o.ToString(), out float value))
{
Lambda = Range.LambdaOf(value);
}
};
}
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Just a tick box
/// </summary>
public class CUITickBox : CUIComponent
{
public GUISoundType ClickSound { get; set; } = GUISoundType.TickBox;
public event Action<bool> OnStateChange;
public void AddOnStateChange(Action<bool> callback) => OnStateChange += callback;
public CUISprite OnSprite { get; set; }
public CUISprite OffSprite { get; set; }
public CUISprite HoverOffSprite { get; set; }
public CUISprite HoverOnSprite { get; set; }
public CUISprite DisabledSprite { get; set; }
private bool state; public bool State
{
get => state;
set
{
if (state == value) return;
state = value;
ChangeSprite();
}
}
public override bool Disabled
{
get => disabled;
set
{
disabled = value;
ChangeSprite();
}
}
public virtual void ChangeSprite()
{
if (Disabled)
{
BackgroundSprite = DisabledSprite;
return;
}
if (State)
{
BackgroundSprite = OnSprite;
if (MouseOver) BackgroundSprite = HoverOnSprite;
}
else
{
BackgroundSprite = OffSprite;
if (MouseOver) BackgroundSprite = HoverOffSprite;
}
}
public CUITickBox() : base()
{
Absolute = new CUINullRect(w: 20, h: 20);
BackgroundColor = Color.Cyan;
Border.Color = Color.Transparent;
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
OffSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(0, 0, 32, 32),
};
OnSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(32, 0, 32, 32),
};
HoverOffSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(64, 0, 32, 32),
};
HoverOnSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(96, 0, 32, 32),
};
DisabledSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(128, 0, 32, 32),
};
ChangeSprite();
OnMouseDown += (e) =>
{
if (Disabled) return;
SoundPlayer.PlayUISound(ClickSound);
State = !State;
OnStateChange?.Invoke(State);
if (Command != null) DispatchUp(new CUICommand(Command, State));
};
OnMouseEnter += (e) => ChangeSprite();
OnMouseLeave += (e) => ChangeSprite();
OnConsume += (o) =>
{
if (o is bool b) State = b;
};
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Just an example of what CUICanvas can be used for
/// </summary>
public class CUIWater : CUICanvas
{
public float Omega = 1.999f;
public float[,] Pool1;
public float[,] Pool2;
public float[,] DensityMap;
public Color[] ColorPalette = new Color[]{
new Color(0,0,0,0),
new Color(0,0,64),
new Color(32,0,64),
new Color(255,0,255),
new Color(0,255,255),
};
public override Point Size
{
get => base.Size;
set
{
base.Size = value;
Pool1 = new float[Texture.Width, Texture.Height];
Pool2 = new float[Texture.Width, Texture.Height];
DensityMap = new float[Texture.Width, Texture.Height];
RandomizeDensityMap();
}
}
public float NextAmplitude(int x, int y)
{
float avg = (
Pool1[x + 1, y] +
Pool1[x, y + 1] +
Pool1[x - 1, y] +
Pool1[x, y - 1]
) / 4.0f;
return avg * Omega + (1 - Omega) * Pool2[x, y];
}
public void Step()
{
for (int x = 1; x < Size.X - 1; x++)
{
for (int y = 1; y < Size.Y - 1; y++)
{
Pool2[x, y] = NextAmplitude(x, y) * DensityMap[x, y];
}
}
(Pool1, Pool2) = (Pool2, Pool1);
}
public double UpdateInterval = 1.0 / 60.0;
private double lastUpdateTime = -1;
public void Update(double totalTime)
{
if (totalTime - lastUpdateTime < UpdateInterval) return;
UpdateSelf();
Step();
lastUpdateTime = totalTime;
TransferData();
}
public virtual void UpdateSelf()
{
}
private void TransferData()
{
for (int x = 0; x < Size.X; x++)
{
for (int y = 0; y < Size.Y; y++)
{
SetPixel(x, y, ToolBox.GradientLerp(Math.Abs(Pool1[x, y]), ColorPalette));
}
}
SetData();
}
public void RandomizeDensityMap()
{
for (int x = 0; x < Size.X; x++)
{
for (int y = 0; y < Size.Y; y++)
{
DensityMap[x, y] = 1.0f - CUI.Random.NextSingle() * 0.01f;
}
}
}
public float DropSize = 16.0f;
public void Drop(float x, float y)
{
int i = (int)Math.Clamp(Math.Round(x * Texture.Width), 1, Texture.Width - 2);
int j = (int)Math.Clamp(Math.Round(y * Texture.Height), 1, Texture.Height - 2);
Pool1[i, j] = DropSize;
}
public CUIWater(int x, int y) : base(x, y)
{
//ConsumeDragAndDrop = true;
//OnUpdate += Update;
Pool1 = new float[Texture.Width, Texture.Height];
Pool2 = new float[Texture.Width, Texture.Height];
DensityMap = new float[Texture.Width, Texture.Height];
RandomizeDensityMap();
// OnMouseOn += (e) =>
// {
// if (!MousePressed) return;
// Vector2 v = CUIAnchor.AnchorFromPos(Real, e.MousePosition);
// Drop(v.X, v.Y);
// };
}
public CUIWater() : this(256, 256)
{
}
}
}

View File

@@ -0,0 +1,95 @@
#define CUIDEBUG
// #define SHOWPERF
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public static class CUIDebug
{
public static bool PrintKeys;
#if !CUIDEBUG
[Conditional("DONT")]
#endif
public static void Log(object msg, Color? cl = null)
{
if (!CUI.Debug) return;
cl ??= Color.Yellow;
LuaCsLogger.LogMessage($"{msg ?? "null"}", cl * 0.8f, cl);
}
#if !CUIDEBUG
[Conditional("DONT")]
#endif
public static void Info(object msg, Color? cl = null, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
if (!CUI.Debug) return;
cl ??= Color.Cyan;
var fi = new FileInfo(source);
CUI.Log($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", cl * 0.5f);
CUI.Log(msg, cl);
}
#if !CUIDEBUG
[Conditional("DONT")]
#endif
public static void Error(object msg, Color? cl = null, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
if (!CUI.Debug) return;
cl ??= Color.Orange;
var fi = new FileInfo(source);
CUI.Log($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", cl * 0.5f);
CUI.Log(msg, cl);
}
#if !CUIDEBUG
[Conditional("DONT")]
#endif
public static void Capture(CUIComponent host, CUIComponent target, string method, string sprop, string tprop, string value)
{
if (target == null || target.IgnoreDebug || !target.Debug) return;
//CUI.Log($"{host} {target} {method} {sprop} {tprop} {value}");
CUIDebugWindow.Main?.Capture(new CUIDebugEvent(host, target, method, sprop, tprop, value));
}
#if !CUIDEBUG
[Conditional("DONT")]
#endif
public static void Flush() => CUIDebugWindow.Main?.Flush();
// public static int CUIShowperfCategory = 1000;
// #if (!SHOWPERF || !CUIDEBUG)
// [Conditional("DONT")]
// #endif
// public static void CaptureTicks(double ticks, string name, int hash) => ShowPerfExtensions.Plugin.CaptureTicks(ticks, CUIShowperfCategory, name, hash);
// #if (!SHOWPERF || !CUIDEBUG)
// [Conditional("DONT")]
// #endif
// public static void CaptureTicks(double ticks, string name) => ShowPerfExtensions.Plugin.CaptureTicks(ticks, CUIShowperfCategory, name);
// #if (!SHOWPERF || !CUIDEBUG)
// [Conditional("DONT")]
// #endif
// public static void EnsureCategory() => ShowPerfExtensions.Plugin.EnsureCategory(CUIShowperfCategory);
}
}

View File

@@ -0,0 +1,155 @@
#define CUIDEBUG
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUIDebugEvent
{
public CUIComponent Host;
public CUIComponent Target;
public string Method;
public string SProp;
public string TProp;
public string Value;
public CUIDebugEvent(CUIComponent host, CUIComponent target, string method, string sprop, string tprop, string value)
{
Host = host;
Target = target;
Method = method ?? "";
SProp = sprop ?? "";
TProp = tprop ?? "";
Value = value ?? "";
}
}
public class CUIDebugEventComponent : CUIComponent
{
public static Dictionary<int, Color> CapturedIDs = new Dictionary<int, Color>();
private CUIDebugEvent _value; public CUIDebugEvent Value
{
get => _value;
set
{
_value = value;
Revealed = value != null;
if (value != null)
{
LastUpdate = Timing.TotalTime;
AssignColor();
}
MakeText();
}
}
public void Flush() => Value = null;
private void MakeText()
{
if (Value == null)
{
Line1 = "";
Line2 = "";
}
else
{
Line1 = $" {Value.Target} in {Value.Host}.{Value.Method}";
Line2 = $" {Value.SProp} -> {Value.TProp} {Value.Value}";
}
}
public static Random random = new Random();
private static float NextColor;
private static float ColorShift = 0.05f;
private void AssignColor()
{
if (Value.Target == null) return;
if (CapturedIDs.ContainsKey(Value.Target.ID))
{
BackgroundColor = CapturedIDs[Value.Target.ID];
}
else
{
// float r = random.NextSingle();
// float scale = 20;
// r = (float)Math.Round(r * scale) / scale;
CapturedIDs[Value.Target.ID] = GetColor(NextColor);
NextColor += ColorShift;
if (NextColor > 1) NextColor = 0;
BackgroundColor = CapturedIDs[Value.Target.ID];
}
}
public string Line1 = "";
public string Line2 = "";
public float UpdateTimer;
public double LastUpdate;
public Color GetColor(float d)
{
return ToolBox.GradientLerp(d,
Color.Cyan * 0.5f,
Color.Red * 0.5f,
Color.Green * 0.5f,
Color.Blue * 0.5f,
Color.Magenta * 0.5f,
Color.Yellow * 0.5f,
Color.Cyan * 0.5f
);
}
public Color GetColor2(float d)
{
return ToolBox.GradientLerp(Math.Min(0.8f, d),
CapturedIDs[Value.Target.ID],
Color.Black * 0.5f
);
}
public override void Draw(SpriteBatch spriteBatch)
{
BackgroundColor = GetColor2((float)(Timing.TotalTime - LastUpdate));
base.Draw(spriteBatch);
GUIStyle.Font.Value.DrawString(spriteBatch, Line1, Real.Position, Color.White, rotation: 0, origin: Vector2.Zero, 0.9f, se: SpriteEffects.None, layerDepth: 0.1f);
GUIStyle.Font.Value.DrawString(spriteBatch, Line2, Real.Position + new Vector2(0, 20), Color.White, rotation: 0, origin: Vector2.Zero, 0.9f, se: SpriteEffects.None, layerDepth: 0.1f);
}
public CUIDebugEventComponent(CUIDebugEvent value = null) : base()
{
Value = value;
IgnoreDebug = true;
BackgroundColor = Color.Green;
Absolute = new CUINullRect(null, null, null, 40);
}
}
}

View File

@@ -0,0 +1,223 @@
#define CUIDEBUG
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUIDebugWindow : CUIFrame
{
public static CUIDebugWindow Main;
public CUIVerticalList EventsComponent;
public CUIVerticalList DebugIDsComponent;
public CUIPages Pages;
public CUIMultiButton PickIDButton;
public List<CUIDebugEventComponent> Events = new List<CUIDebugEventComponent>();
public int target;
public bool Loop { get; set; } = true;
public void Capture(CUIDebugEvent e)
{
if (EventsComponent == null) return;
if (target > 200) return;
if (Events.Count < target + 1)
{
CUIDebugEventComponent ec = new CUIDebugEventComponent(e);
Events.Add(ec);
EventsComponent.Append(ec);
ec.OnMouseEnter += (m) => ec.Value.Target.DebugHighlight = true;
ec.OnMouseLeave += (m) => ec.Value.Target.DebugHighlight = false;
}
else
{
Events[target].Value = e;
}
target++;
}
public void Flush()
{
if (Loop) target = 0;
//Events.ForEach(e => e.Flush());
}
public void MakeIDList()
{
DebugIDsComponent.Visible = false;
DebugIDsComponent.RemoveAllChildren();
List<CUIComponent> l = new List<CUIComponent>();
if (CUI.Main is not null)
{
RunRecursiveOn(CUI.Main, (component) =>
{
if (!component.IgnoreDebug) l.Add(component);
});
}
foreach (CUIComponent c in l)
{
CUIToggleButton b = new CUIToggleButton(c.ToString())
{
State = c.Debug,
IgnoreDebug = true,
Style = new CUIStyle(){
{"TextAlign", "[0,0]"}
},
AddOnMouseDown = (m) =>
{
c.Debug = !c.Debug;
MakeIDList();
},
AddOnMouseEnter = (m) => c.DebugHighlight = true,
AddOnMouseLeave = (m) => c.DebugHighlight = false,
};
DebugIDsComponent.Append(b);
}
DebugIDsComponent.Visible = true;
l.Clear();
}
public CUIDebugWindow() : base()
{
this.ZIndex = 1000;
this.Layout = new CUILayoutVerticalList();
this["handle"] = new CUIComponent()
{
Absolute = new CUINullRect(null, null, null, 20),
};
this["handle"]["closebutton"] = new CUIButton("X")
{
Anchor = new Vector2(1, 0.5f),
Style = new CUIStyle(){
{"MasterColor", "Red"}
},
AddOnMouseDown = (e) =>
{
CUIDebugWindow.Close();
},
};
this["controls"] = new CUIComponent()
{
FitContent = new CUIBool2(false, true),
};
this["controls"]["loop"] = new CUIToggleButton("loop")
{
Relative = new CUINullRect(0, 0, 0.5f, null),
AddOnStateChange = (state) =>
{
Loop = state;
Events?.Clear();
EventsComponent?.RemoveAllChildren();
},
State = Loop,
};
this["controls"].Append(PickIDButton = new CUIMultiButton()
{
Relative = new CUINullRect(0.5f, 0, 0.5f, null),
Style = new CUIStyle(){
{"InactiveColor", "0,0,0,128"},
{"MousePressedColor", "0,255,255,64"}
},
ConsumeDragAndDrop = false,
Options = new string[]{
"Debug events", "Debugged components"
}
});
Append(Pages = new CUIPages()
{
FillEmptySpace = new CUIBool2(false, true),
Style = new CUIStyle(){
{"BackgroundColor", "0,0,32,128"}
},
IgnoreDebug = true,
});
EventsComponent = new CUIVerticalList()
{
Relative = new CUINullRect(0, 0, 1, 1),
Scrollable = true,
IgnoreDebug = true,
};
DebugIDsComponent = new CUIVerticalList()
{
Relative = new CUINullRect(0, 0, 1, 1),
Scrollable = true,
IgnoreDebug = true,
};
PickIDButton.OnSelect += (s) =>
{
if (PickIDButton.SelectedIndex == 0)
{
MakeIDList();
Pages.Open(EventsComponent);
}
else Pages.Open(DebugIDsComponent);
};
PickIDButton.Select(0);
this["controls"].Get<CUIToggleButton>("loop").State = true;
IgnoreDebug = true;
}
public static CUIDebugWindow Open()
{
if (CUI.Main == null) return null;
CUIDebugWindow w = new CUIDebugWindow()
{
Absolute = new CUINullRect(10, 370, 500, 370),
};
CUI.Main.Append(w);
CUIDebugWindow.Main = w;
CUI.Main.OnTreeChanged += () => w.MakeIDList();
return w;
}
public static void Close()
{
if (CUIDebugWindow.Main == null) return;
CUIDebugWindow.Main.RemoveSelf();
CUIDebugWindow.Main.Revealed = false;
CUIDebugWindow.Main = null;
}
public CUIDebugWindow(float? x = null, float? y = null, float? w = null, float? h = null) : this()
{
Relative = new CUINullRect(x, y, w, h);
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUIDragHandle : ICUIVitalizable
{
public void SetHost(CUIComponent host) => Host = host;
public CUIComponent Host;
public Vector2 GrabOffset;
public bool Grabbed;
public bool Draggable;
public CUIMouseEvent Trigger = CUIMouseEvent.Down;
/// <summary>
/// If true, will change relative prop instead of Absolute
/// </summary>
public bool DragRelative { get; set; } = false;
public bool OutputRealPos { get; set; } = false;
public bool ShouldStart(CUIInput input)
{
return Draggable && (
(Trigger == CUIMouseEvent.Down && input.MouseDown) ||
(Trigger == CUIMouseEvent.DClick && input.DoubleClick)
);
}
public void BeginDrag(Vector2 cursorPos)
{
Grabbed = true;
GrabOffset = cursorPos - CUIAnchor.PosIn(Host);
}
public void EndDrag()
{
Grabbed = false;
Host.MainComponent?.OnDragEnd(this);
}
//TODO test in 3d child offset
public void DragTo(Vector2 to)
{
Vector2 pos = Host.Parent.ChildrenOffset.ToPlaneCoords(
to - GrabOffset - CUIAnchor.PosIn(Host.Parent.Real, Host.ParentAnchor ?? Host.Anchor)
);
if (DragRelative)
{
Vector2 newRelPos = new Vector2(
pos.X / Host.Parent.Real.Width,
pos.Y / Host.Parent.Real.Height
);
Host.CUIProps.Relative.SetValue(Host.Relative with { Position = newRelPos });
Host.InvokeOnDrag(newRelPos.X, newRelPos.Y);
}
else
{
Host.CUIProps.Absolute.SetValue(Host.Absolute with { Position = pos });
if (OutputRealPos) Host.InvokeOnDrag(to.X, to.Y);
else Host.InvokeOnDrag(pos.X, pos.Y);
}
}
public CUIDragHandle() { }
public CUIDragHandle(CUIComponent host) => Host = host;
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUIFocusHandle : ICUIVitalizable
{
public void SetHost(CUIComponent host) => Host = host;
public CUIComponent Host;
public bool Focusable;
public CUIMouseEvent Trigger = CUIMouseEvent.Down;
public bool ShouldStart(CUIInput input)
{
return Focusable && (
(Trigger == CUIMouseEvent.Down && input.MouseDown) ||
(Trigger == CUIMouseEvent.DClick && input.DoubleClick)
);
}
public CUIFocusHandle() { }
public CUIFocusHandle(CUIComponent host) => Host = host;
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Containing a snapshot of current mouse and keyboard state
/// </summary>
public class CUIInput
{
public static double DoubleClickInterval = 0.2;
public static float ScrollSpeed = 0.6f;
public MouseState Mouse;
public bool MouseDown;
public bool DoubleClick;
public bool MouseUp;
public bool MouseHeld;
public float Scroll;
public bool Scrolled;
public Vector2 MousePosition;
public Vector2 MousePositionDif;
public bool MouseMoved;
//TODO split into sh mouse and sh keyboard
public bool SomethingHappened;
//HACK rethink, this is too hacky
public bool ClickConsumed;
public KeyboardState Keyboard;
public Keys[] HeldKeys = new Keys[0];
public Keys[] PressedKeys = new Keys[0];
public Keys[] UnpressedKeys = new Keys[0];
public bool SomeKeyHeld;
public bool SomeKeyPressed;
public bool SomeKeyUnpressed;
public TextInputEventArgs[] WindowTextInputEvents;
public TextInputEventArgs[] WindowKeyDownEvents;
public bool SomeWindowEvents;
//-------------- private stuff
private double PrevMouseDownTiming;
private int PrevScrollWheelValue;
private MouseState PrevMouseState;
private Vector2 PrevMousePosition;
private Keys[] PrevHeldKeys = new Keys[0];
private Queue<TextInputEventArgs> WindowTextInputQueue = new Queue<TextInputEventArgs>(10);
private Queue<TextInputEventArgs> WindowKeyDownQueue = new Queue<TextInputEventArgs>(10);
//HACK super hacky solution to block input from one CUIMainComponent to another
public bool MouseInputHandled { get; set; }
public void Scan(double totalTime)
{
MouseInputHandled = false;
ScanMouse(totalTime);
ScanKeyboard(totalTime);
}
private void ScanMouse(double totalTime)
{
ClickConsumed = false;
Mouse = Microsoft.Xna.Framework.Input.Mouse.GetState();
MouseDown = PrevMouseState.LeftButton == ButtonState.Released && Mouse.LeftButton == ButtonState.Pressed;
MouseUp = PrevMouseState.LeftButton == ButtonState.Pressed && Mouse.LeftButton == ButtonState.Released;
MouseHeld = Mouse.LeftButton == ButtonState.Pressed;
PrevMousePosition = MousePosition;
MousePosition = new Vector2(Mouse.Position.X, Mouse.Position.Y);
MousePositionDif = MousePosition - PrevMousePosition;
MouseMoved = MousePositionDif != Vector2.Zero;
Scroll = (Mouse.ScrollWheelValue - PrevScrollWheelValue) * ScrollSpeed;
PrevScrollWheelValue = Mouse.ScrollWheelValue;
Scrolled = Scroll != 0;
DoubleClick = false;
if (MouseDown)
{
if (totalTime - PrevMouseDownTiming < DoubleClickInterval)
{
DoubleClick = true;
}
PrevMouseDownTiming = totalTime;
}
SomethingHappened = MouseHeld || MouseUp || MouseDown || MouseMoved || Scrolled;
PrevMouseState = Mouse;
}
private void ScanKeyboard(double totalTime)
{
Keyboard = Microsoft.Xna.Framework.Input.Keyboard.GetState();
HeldKeys = Keyboard.GetPressedKeys();
SomeKeyHeld = HeldKeys.Length > 0;
PressedKeys = HeldKeys.Except(PrevHeldKeys).ToArray();
UnpressedKeys = PrevHeldKeys.Except(HeldKeys).ToArray();
SomeKeyPressed = PressedKeys.Length > 0;
SomeKeyUnpressed = UnpressedKeys.Length > 0;
PrevHeldKeys = HeldKeys;
WindowTextInputEvents = WindowTextInputQueue.ToArray();
WindowTextInputQueue.Clear();
WindowKeyDownEvents = WindowKeyDownQueue.ToArray();
WindowKeyDownQueue.Clear();
SomeWindowEvents = WindowTextInputEvents.Length > 0 || WindowKeyDownEvents.Length > 0;
}
public CUIInput()
{
CUI.OnWindowKeyDown += (e) => WindowKeyDownQueue.Enqueue(e);
CUI.OnWindowTextInput += (e) => WindowTextInputQueue.Enqueue(e);
}
}
}

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUIResizeHandle : ICUIVitalizable
{
public void SetHost(CUIComponent host) => Host = host;
public CUIComponent Host;
public CUIRect Real;
public Vector2 Anchor;
public Vector2 StaticPointAnchor;
public Vector2 AnchorDif;
public CUINullRect Absolute;
public CUISprite Sprite;
public Vector2 MemoStaticPoint;
public bool Grabbed;
public bool Visible = false;
public CUIBool2 Direction { get; set; } = new CUIBool2(true, true);
public CUIMouseEvent Trigger = CUIMouseEvent.Down;
public bool ShouldStart(CUIInput input)
{
return Visible && Real.Contains(input.MousePosition) && (
(Trigger == CUIMouseEvent.Down && input.MouseDown) ||
(Trigger == CUIMouseEvent.DClick && input.DoubleClick)
);
}
public void BeginResize(Vector2 cursorPos)
{
Grabbed = true;
MemoStaticPoint = CUIAnchor.PosIn(Host.Real, StaticPointAnchor);
}
public void EndResize()
{
Grabbed = false;
Host.MainComponent?.OnResizeEnd(this);
}
public void Resize(Vector2 cursorPos)
{
float limitedX;
if (CUIAnchor.Direction(StaticPointAnchor).X >= 0)
{
limitedX = Math.Max(MemoStaticPoint.X + Real.Width, cursorPos.X);
}
else
{
limitedX = Math.Min(MemoStaticPoint.X - Real.Width, cursorPos.X);
}
float limitedY;
if (CUIAnchor.Direction(StaticPointAnchor).Y >= 0)
{
limitedY = Math.Max(MemoStaticPoint.Y + Real.Height, cursorPos.Y);
}
else
{
limitedY = Math.Min(MemoStaticPoint.Y - Real.Height, cursorPos.Y);
}
Vector2 LimitedCursorPos = new Vector2(limitedX, limitedY);
Vector2 RealDif = MemoStaticPoint - LimitedCursorPos;
Vector2 SizeFactor = RealDif / AnchorDif;
Vector2 TopLeft = MemoStaticPoint - SizeFactor * StaticPointAnchor;
Vector2 newSize = new Vector2(
Math.Max(Real.Width, SizeFactor.X),
Math.Max(Real.Height, SizeFactor.Y)
);
Vector2 newPos = TopLeft - CUIAnchor.PosIn(Host.Parent.Real, Host.ParentAnchor ?? Host.Anchor) + CUIAnchor.PosIn(new CUIRect(newSize), Host.Anchor);
if (Direction.X) Host.CUIProps.Absolute.SetValue(new CUINullRect(newPos.X, Host.Absolute.Top, newSize.X, Host.Absolute.Height));
if (Direction.Y) Host.CUIProps.Absolute.SetValue(new CUINullRect(Host.Absolute.Left, newPos.Y, Host.Absolute.Width, newSize.Y));
}
public void Update()
{
if (!Visible) return;
float x, y, w, h;
x = y = w = h = 0;
if (Absolute.Left.HasValue) x = Absolute.Left.Value;
if (Absolute.Top.HasValue) y = Absolute.Top.Value;
if (Absolute.Width.HasValue) w = Absolute.Width.Value;
if (Absolute.Height.HasValue) h = Absolute.Height.Value;
Vector2 Pos = CUIAnchor.GetChildPos(Host.Real, Anchor, new Vector2(x, y), new Vector2(w, h));
Real = new CUIRect(Pos, new Vector2(w, h));
}
public void Draw(SpriteBatch spriteBatch)
{
if (!Visible) return;
CUI.DrawRectangle(spriteBatch, Real, Grabbed ? Host.ResizeHandleGrabbedColor : Host.ResizeHandleColor, Sprite);
}
public CUIResizeHandle(Vector2 anchor, CUIBool2 flipped)
{
if (anchor == CUIAnchor.Center)
{
CUI.Log($"Pls don't use CUIAnchor.Center for CUIResizeHandle, it makes no sense:\nThe StaticPointAnchor is symetric to Anchor and in this edge case == Anchor");
}
Anchor = anchor;
StaticPointAnchor = Vector2.One - Anchor;
AnchorDif = StaticPointAnchor - Anchor;
Absolute = new CUINullRect(0, 0, 15, 15);
Sprite = CUI.TextureManager.GetSprite(CUI.CUITexturePath);
Sprite.SourceRect = new Rectangle(0, 32, 32, 32);
if (flipped.X) Sprite.Effects |= SpriteEffects.FlipHorizontally;
if (flipped.Y) Sprite.Effects |= SpriteEffects.FlipVertically;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUISwipeHandle : ICUIVitalizable
{
public void SetHost(CUIComponent host) => Host = host;
public CUIComponent Host;
public bool Grabbed;
public bool Swipeable;
public Vector2 PrevPosition;
public CUIMouseEvent Trigger = CUIMouseEvent.Down;
public bool ShouldStart(CUIInput input)
{
return Swipeable && (
(Trigger == CUIMouseEvent.Down && input.MouseDown) ||
(Trigger == CUIMouseEvent.DClick && input.DoubleClick)
);
}
public void BeginSwipe(Vector2 cursorPos)
{
Grabbed = true;
PrevPosition = cursorPos;
}
public void EndSwipe()
{
Grabbed = false;
Host.MainComponent?.OnSwipeEnd(this);
}
public void Swipe(CUIInput input)
{
Host.CUIProps.ChildrenOffset.SetValue(
Host.ChildrenOffset.Shift(
input.MousePositionDif.X,
input.MousePositionDif.Y
)
);
Host.InvokeOnSwipe(input.MousePositionDif.X, input.MousePositionDif.Y);
}
public CUISwipeHandle() { }
public CUISwipeHandle(CUIComponent host) => Host = host;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
public class CUIWeakEvent
{
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System.IO;
namespace QICrabUI
{
public partial class CUI
{
/// <summary>
/// $"‖color:{color}‖{msg}‖end‖"
/// </summary>
/// <param name="msg"></param>
/// <param name="color"></param>
/// <returns></returns>
public static string WrapInColor(object msg, string color)
{
return $"‖color:{color}‖{msg}‖end‖";
}
//HACK too lazy to make good name
/// <summary>
/// Serializes the array
/// </summary>
/// <param name="array"></param>
/// <returns></returns>
public static string ArrayToString(IEnumerable<object> array)
{
return $"[{String.Join(", ", array.Select(o => o.ToString()))}]";
}
/// <summary>
/// Prints a message to console
/// </summary>
/// <param name="msg"></param>
/// <param name="color"></param>
public static void Log(object msg, Color? color = null, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
color ??= Color.Cyan;
// var fi = new FileInfo(source);
// LuaCsLogger.LogMessage($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", color * 0.6f, color * 0.6f);
LuaCsLogger.LogMessage($"{msg ?? "null"}", color * 0.8f, color);
}
public static void Warning(object msg, Color? color = null, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
color ??= Color.Yellow;
// var fi = new FileInfo(source);
// LuaCsLogger.LogMessage($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", color * 0.6f, color * 0.6f);
LuaCsLogger.LogMessage($"{msg ?? "null"}", color * 0.8f, color);
}
/// <summary>
/// xd
/// </summary>
/// <param name="source"> This should be injected by compiler, don't set </param>
/// <returns></returns>
public static string GetCallerFolderPath([CallerFilePath] string source = "") => Path.GetDirectoryName(source);
/// <summary>
/// Prints debug message with source path
/// Works only if debug is true
/// </summary>
/// <param name="msg"></param>
public static void Info(object msg, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
if (Debug == true)
{
var fi = new FileInfo(source);
Log($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", Color.Yellow * 0.5f);
Log(msg, Color.Yellow);
}
}
}
}

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using HarmonyLib;
using EventInput;
namespace QICrabUI
{
public partial class CUI
{
public static void CheckOtherPatches(string msg = "")
{
CUI.Log(msg);
CUI.Log($"Harmony.GetAllPatchedMethods:", Color.Lime);
foreach (MethodBase mb in Harmony.GetAllPatchedMethods())
{
Patches patches = Harmony.GetPatchInfo(mb);
if (patches.Prefixes.Count() > 0 || patches.Postfixes.Count() > 0)
{
CUI.Log($"{mb.DeclaringType}.{mb.Name}:");
if (patches.Prefixes.Count() > 0)
{
CUI.Log($" Prefixes:");
foreach (Patch patch in patches.Prefixes) { CUI.Log($" {patch.owner}"); }
}
if (patches.Postfixes.Count() > 0)
{
CUI.Log($" Postfixes:");
foreach (Patch patch in patches.Postfixes) { CUI.Log($" {patch.owner}"); }
}
}
}
}
public static void CheckPatches(string typeName, string methodName)
{
CUI.Log($"Harmony.GetAllPatchedMethods:", Color.Lime);
foreach (MethodBase mb in Harmony.GetAllPatchedMethods())
{
if (
!string.Equals(typeName, mb.DeclaringType.Name, StringComparison.OrdinalIgnoreCase) ||
!string.Equals(methodName, mb.Name, StringComparison.OrdinalIgnoreCase)
) continue;
Patches patches = Harmony.GetPatchInfo(mb);
if (patches.Prefixes.Count() > 0 || patches.Postfixes.Count() > 0)
{
CUI.Log($"{mb.DeclaringType}.{mb.Name}:");
if (patches.Prefixes.Count() > 0)
{
CUI.Log($" Prefixes:");
foreach (Patch patch in patches.Prefixes) { CUI.Log($" {patch.owner}"); }
}
if (patches.Postfixes.Count() > 0)
{
CUI.Log($" Postfixes:");
foreach (Patch patch in patches.Postfixes) { CUI.Log($" {patch.owner}"); }
}
}
}
}
private static void PatchAll()
{
GameMain.LuaCs.Hook.Add("GUI_Draw_Prefix", CUIHookID, (object[] args) =>
{
GUI_Draw_Prefix((SpriteBatch)args.ElementAtOrDefault(0));
return null;
});
GameMain.LuaCs.Hook.Add("GUI_DrawCursor_Prefix", CUIHookID, (object[] args) =>
{
GUI_DrawCursor_Prefix((SpriteBatch)args.ElementAtOrDefault(0));
return null;
});
GameMain.LuaCs.Hook.Add("think", CUIHookID, (object[] args) =>
{
CUIUpdateMouseOn();
CUIUpdate(Timing.TotalTime);
return null;
});
// this hook seems to do nothing
// GameMain.LuaCs.Hook.Add("Camera_MoveCamera_Prefix", CUIHookID, (object[] args) =>
// {
// return Camera_MoveCamera_Prefix(); ;
// });
GameMain.LuaCs.Hook.Add("KeyboardDispatcher_set_Subscriber_Prefix", CUIHookID, (object[] args) =>
{
KeyboardDispatcher_set_Subscriber_Prefix(
(KeyboardDispatcher)args.ElementAtOrDefault(0),
(IKeyboardSubscriber)args.ElementAtOrDefault(1)
);
return null;
});
GameMain.LuaCs.Hook.Add("GUI_InputBlockingMenuOpen_Postfix", CUIHookID, (object[] args) =>
{
return GUI_InputBlockingMenuOpen_Postfix();
});
GameMain.LuaCs.Hook.Add("GUI_TogglePauseMenu_Postfix", CUIHookID, (object[] args) =>
{
GUI_TogglePauseMenu_Postfix();
return null;
});
}
private static void GameMain_Update_Postfix(GameTime gameTime)
{
CUIUpdate(gameTime.TotalGameTime.TotalSeconds);
}
private static void CUIUpdate(double time)
{
if (Main == null) CUI.Error($"CUIUpdate: CUI.Main in {HookIdentifier} was null, tell the dev", 20);
try
{
CUIAnimation.UpdateAllAnimations(time);
CUI.Input?.Scan(time);
TopMain?.Update(time);
Main?.Update(time);
}
catch (Exception e)
{
CUI.Warning($"CUI: {e}");
}
}
private static void GUI_Draw_Prefix(SpriteBatch spriteBatch)
{
try { Main?.Draw(spriteBatch); }
catch (Exception e) { CUI.Warning($"CUI: {e}"); }
}
private static void GUI_DrawCursor_Prefix(SpriteBatch spriteBatch)
{
try { TopMain?.Draw(spriteBatch); }
catch (Exception e) { CUI.Warning($"CUI: {e}"); }
}
private static void GUI_UpdateMouseOn_Postfix(ref GUIComponent __result)
{
CUIUpdateMouseOn();
}
private static void CUIUpdateMouseOn()
{
if (Main == null) CUI.Error($"CUIUpdateMouseOn: CUI.Main in {HookIdentifier} was null, tell the dev", 20);
if (GUI.MouseOn == null && Main != null && Main.MouseOn != null && Main.MouseOn != Main) GUI.MouseOn = CUIComponent.dummyComponent;
if (TopMain != null && TopMain.MouseOn != null && TopMain.MouseOn != TopMain) GUI.MouseOn = CUIComponent.dummyComponent;
}
private static Dictionary<string, bool> Camera_MoveCamera_Prefix()
{
if (GUI.MouseOn != CUIComponent.dummyComponent) return null;
return new Dictionary<string, bool>()
{
["allowZoom"] = false,
};
}
private static void KeyboardDispatcher_set_Subscriber_Prefix(KeyboardDispatcher __instance, IKeyboardSubscriber value)
{
FocusResolver?.OnVanillaIKeyboardSubscriberSet(value);
}
public static bool GUI_InputBlockingMenuOpen_Postfix()
{
return CUI.InputBlockingMenuOpen;
}
public static void GUI_TogglePauseMenu_Postfix()
{
try
{
if (GUI.PauseMenu != null)
{
GUIFrame frame = GUI.PauseMenu;
GUIComponent pauseMenuInner = frame.GetChild(1);
GUIComponent list = frame.GetChild(1).GetChild(0);
GUIButton resumeButton = (GUIButton)list.GetChild(0);
GUIButton.OnClickedHandler oldHandler = resumeButton.OnClicked;
resumeButton.OnClicked = (GUIButton button, object obj) =>
{
bool guh = oldHandler(button, obj);
CUI.InvokeOnPauseMenuToggled();
return guh;
};
}
}
catch (Exception e) { CUI.Warning(e); }
CUI.InvokeOnPauseMenuToggled();
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System.IO;
namespace QICrabUI
{
public partial class CUI
{
//Idk, not very usefull
/// <summary>
/// Just an experimant
/// Creates empty CUIComponent from class name
/// </summary>
/// <param name="componentName"></param>
/// <returns></returns>
public static CUIComponent Create(string componentName)
{
return (CUIComponent)Activator.CreateInstance(CUIReflection.GetComponentTypeByName(componentName));
}
}
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUI
{
internal static List<DebugConsole.Command> AddedCommands = new List<DebugConsole.Command>();
internal static void AddCommands()
{
AddedCommands.Add(new DebugConsole.Command("cuidebug", "", CUIDebug_Command));
AddedCommands.Add(new DebugConsole.Command("cuicreatepalette", "cuicreatepalette name frontcolor [backcolor]", CUICreatePalette_Command));
AddedCommands.Add(new DebugConsole.Command("cuimg", "", CUIMG_Command));
AddedCommands.Add(new DebugConsole.Command("cuidraworder", "", CUIDrawOrder_Command));
AddedCommands.Add(new DebugConsole.Command("cuiprinttree", "", CUIPrintTree_Command));
AddedCommands.Add(new DebugConsole.Command("printsprites", "", PrintSprites_Command));
AddedCommands.Add(new DebugConsole.Command("printkeys", "", PrintSprites_Command));
AddedCommands.Add(new DebugConsole.Command("cuipalette", "load palette as primary", Palette_Command, () => new string[][] { CUIPalette.LoadedPalettes.Keys.ToArray() }));
AddedCommands.Add(new DebugConsole.Command("cuipalettedemo", "", PaletteDemo_Command));
AddedCommands.Add(new DebugConsole.Command("cuicreatepaletteset", "name primaty secondary tertiary quaternary", CUICreatePaletteSet_Command, () => new string[][] {
new string[]{},
CUIPalette.LoadedPalettes.Keys.ToArray(),
CUIPalette.LoadedPalettes.Keys.ToArray(),
CUIPalette.LoadedPalettes.Keys.ToArray(),
CUIPalette.LoadedPalettes.Keys.ToArray(),
}));
AddedCommands.Add(new DebugConsole.Command("cuiloadpaletteset", "", CUILoadPaletteSet_Command));
AddedCommands.Add(new DebugConsole.Command("cuicreateluatypesfile", "", CUICreateLuaTypesFile_Command));
DebugConsole.Commands.InsertRange(0, AddedCommands);
}
public static void CUICreateLuaTypesFile_Command(string[] args)
{
CUI.LuaRegistrar.ConstructLuaStaticsFile();
}
public static void CUIDebug_Command(string[] args)
{
if (CUIDebugWindow.Main == null)
{
CUIDebugWindow.Open();
}
else
{
CUIDebugWindow.Close();
}
}
public static void CUIDrawOrder_Command(string[] args)
{
foreach (CUIComponent c in CUI.Main.Flat)
{
CUI.Log(c);
}
}
public static void CUIPrintTree_Command(string[] args)
{
CUI.Main?.PrintTree();
CUI.TopMain?.PrintTree();
}
public static void CUICreatePalette_Command(string[] args)
{
string name = args.ElementAtOrDefault(0);
Color colorA = CUIExtensions.ParseColor((args.ElementAtOrDefault(1) ?? "white"));
Color colorB = CUIExtensions.ParseColor((args.ElementAtOrDefault(2) ?? "black"));
CUIPalette palette = CUIPalette.CreatePaletteFromColors(name, colorA, colorB);
CUIPalette.Primary = palette;
}
public static void CUICreatePaletteSet_Command(string[] args)
{
CUIPalette.SaveSet(
args.ElementAtOrDefault(0),
args.ElementAtOrDefault(1),
args.ElementAtOrDefault(2),
args.ElementAtOrDefault(3),
args.ElementAtOrDefault(4)
);
}
public static void CUILoadPaletteSet_Command(string[] args)
{
CUIPalette.LoadSet(Path.Combine(CUIPalette.PaletteSetsPath, args.ElementAtOrDefault(0)));
}
public static void CUIMG_Command(string[] args) => CUIMagnifyingGlass.ToggleEquip();
public static void PrintSprites_Command(string[] args)
{
foreach (GUIComponentStyle style in GUIStyle.ComponentStyles)
{
CUI.Log($"{style.Name} {style.Sprites.Count}");
}
}
public static void PrintKeysCommand(string[] args)
{
CUIDebug.PrintKeys = !CUIDebug.PrintKeys;
if (CUIDebug.PrintKeys)
{
var values = typeof(Keys).GetEnumValues();
foreach (var v in values)
{
Log($"{(int)v} {v}");
}
Log("---------------------------");
}
}
public static void PaletteDemo_Command(string[] args)
{
try { CUIPalette.PaletteDemo(); } catch (Exception e) { CUI.Warning(e); }
}
public static void Palette_Command(string[] args)
{
try
{
CUIPalette palette = CUIPalette.LoadedPalettes?.GetValueOrDefault(args.ElementAtOrDefault(0) ?? "");
if (palette != null) CUIPalette.Primary = palette;
}
catch (Exception e) { CUI.Warning(e); }
}
internal static void RemoveCommands()
{
AddedCommands.ForEach(c => DebugConsole.Commands.Remove(c));
AddedCommands.Clear();
}
// public static void PermitCommands(Identifier command, ref bool __result)
// {
// if (AddedCommands.Any(c => c.Names.Contains(command.Value))) __result = true;
// }
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.IO;
namespace QICrabUI
{
public partial class CUI
{
public const float Pi2 = (float)(Math.PI / 2.0);
public static SamplerState NoSmoothing = new SamplerState()
{
Filter = TextureFilter.Point,
AddressU = TextureAddressMode.Clamp,
AddressV = TextureAddressMode.Clamp,
AddressW = TextureAddressMode.Clamp,
BorderColor = Color.White,
MaxAnisotropy = 4,
MaxMipLevel = 0,
MipMapLevelOfDetailBias = -0.8f,
ComparisonFunction = CompareFunction.Never,
FilterMode = TextureFilterMode.Default,
};
public static void DrawTexture(SpriteBatch sb, CUIRect cuirect, Color cl, Texture2D texture, float depth = 0.0f)
{
Rectangle sourceRect = new Rectangle(0, 0, (int)cuirect.Width, (int)cuirect.Height);
sb.Draw(texture, cuirect.Box, sourceRect, cl, 0.0f, Vector2.Zero, SpriteEffects.None, depth);
}
public static void DrawRectangle(SpriteBatch sb, CUIRect cuirect, Color cl, CUISprite sprite, float depth = 0.0f)
{
Rectangle sourceRect = sprite.DrawMode switch
{
CUISpriteDrawMode.Resize => sprite.SourceRect,
CUISpriteDrawMode.Wrap => new Rectangle(0, 0, (int)cuirect.Width, (int)cuirect.Height),
CUISpriteDrawMode.Static => cuirect.Box,
CUISpriteDrawMode.StaticDeep => cuirect.Zoom(0.9f),
_ => sprite.SourceRect,
};
Rectangle rect = new Rectangle(
(int)(cuirect.Left + sprite.Offset.X * cuirect.Width),
(int)(cuirect.Top + sprite.Offset.Y * cuirect.Height),
(int)(cuirect.Width),
(int)(cuirect.Height)
);
//rect = cuirect.Box;
sb.Draw(sprite.Texture, rect, sourceRect, cl, sprite.Rotation, sprite.Origin, sprite.Effects, depth);
}
//TODO i can calculate those rects in advance
public static void DrawBorders(SpriteBatch sb, CUIComponent component, float depth = 0.0f)
{
Texture2D texture = component.BorderSprite.Texture;
Rectangle sourceRect = texture.Bounds;
Rectangle targetRect;
Color cl;
float rotation = 0.0f;
float thickness = 1.0f;
bool visible = false;
// Right
visible = component.RigthBorder?.Visible ?? component.Border.Visible;
thickness = component.RigthBorder?.Thickness ?? component.Border.Thickness;
cl = component.RigthBorder?.Color ?? component.Border.Color;
targetRect = CUIRect.CreateRect(
component.Real.Left + component.Real.Width,
component.Real.Top,
component.Real.Height,
thickness
);
sourceRect = CUIRect.CreateRect(
0, 0,
targetRect.Width, texture.Height
);
rotation = Pi2;
sb.Draw(texture, targetRect, sourceRect, cl, rotation, Vector2.Zero, SpriteEffects.None, depth);
//Left
visible = component.LeftBorder?.Visible ?? component.Border.Visible;
thickness = component.LeftBorder?.Thickness ?? component.Border.Thickness;
cl = component.LeftBorder?.Color ?? component.Border.Color;
targetRect = CUIRect.CreateRect(
component.Real.Left + thickness,
component.Real.Top,
component.Real.Height,
thickness
);
sourceRect = CUIRect.CreateRect(
0, 0,
targetRect.Width, texture.Height
);
rotation = Pi2;
sb.Draw(texture, targetRect, sourceRect, cl, rotation, Vector2.Zero, SpriteEffects.FlipVertically, depth);
//Top
visible = component.TopBorder?.Visible ?? component.Border.Visible;
thickness = component.TopBorder?.Thickness ?? component.Border.Thickness;
cl = component.TopBorder?.Color ?? component.Border.Color;
targetRect = CUIRect.CreateRect(
component.Real.Left,
component.Real.Top,
component.Real.Width,
thickness
);
sourceRect = CUIRect.CreateRect(
0, 0,
targetRect.Width, texture.Height
);
rotation = 0.0f;
sb.Draw(texture, targetRect, sourceRect, cl, rotation, Vector2.Zero, SpriteEffects.None, depth);
//Bottom
visible = component.BottomBorder?.Visible ?? component.Border.Visible;
thickness = component.BottomBorder?.Thickness ?? component.Border.Thickness;
cl = component.BottomBorder?.Color ?? component.Border.Color;
targetRect = CUIRect.CreateRect(
component.Real.Left,
component.Real.Bottom - thickness,
component.Real.Width,
thickness
);
sourceRect = CUIRect.CreateRect(
0, 0,
targetRect.Width, texture.Height
);
rotation = 0;
sb.Draw(texture, targetRect, sourceRect, cl, rotation, Vector2.Zero, SpriteEffects.FlipVertically, depth);
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System.IO;
namespace QICrabUI
{
public partial class CUI
{
public static Dictionary<string, int> Errors = new();
public static void Error(object msg, int maxPrints = 1, bool silent = false)
{
string s = $"{msg}";
if (!Errors.ContainsKey(s)) Errors[s] = 1;
else Errors[s] = Errors[s] + 1;
if (silent) return;
if (Errors[s] <= maxPrints) Log($"CUI: {s} x{Errors[s]}", Color.Orange);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using System.Globalization;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using HarmonyLib;
namespace QICrabUI
{
// [CUIInternal]
public static partial class CUIExtensions
{
public static Color RandomColor() => new Color(CUI.Random.Next(256), CUI.Random.Next(256), CUI.Random.Next(256));
public static Color GrayScale(int v) => new Color(v, v, v);
public static Color Mult(this Color cl, float f) => new Color((int)(cl.R * f), (int)(cl.G * f), (int)(cl.B * f), cl.A);
public static Color To(this Color colorA, Color colorB, float f) => ToolBox.GradientLerp(f, new Color[] { colorA, colorB });
public static Dictionary<string, Color> GetShades(Color colorA, Color? colorB = null)
{
Color clB = colorB ?? Color.Black;
Dictionary<string, Color> shades = new();
float steps = 6.0f;
shades["0"] = colorA.To(clB, 0.0f / steps);
shades["1"] = colorA.To(clB, 1.0f / steps);
shades["2"] = colorA.To(clB, 2.0f / steps);
shades["3"] = colorA.To(clB, 3.0f / steps);
shades["4"] = colorA.To(clB, 4.0f / steps);
shades["5"] = colorA.To(clB, 5.0f / steps);
shades["6"] = colorA.To(clB, 6.0f / steps);
return shades;
}
public static void GeneratePaletteFromColors(Color colorA, Color colorB)
{
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using System.Globalization;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using HarmonyLib;
namespace QICrabUI
{
[CUIInternal]
public static partial class CUIExtensions
{
public static int Fit(this int i, int bottom, int top) => Math.Max(bottom, Math.Min(i, top));
public static Vector2 Rotate(this Vector2 v, float angle) => Vector2.Transform(v, Matrix.CreateRotationZ(angle));
public static string SubstringSafe(this string s, int start)
{
try
{
int safeStart = start.Fit(0, s.Length);
return s.Substring(safeStart, s.Length - safeStart);
}
catch (Exception e)
{
CUI.Log($"SubstringSafe {e}");
return "";
}
}
public static string SubstringSafe(this string s, int start, int length)
{
int end = (start + length).Fit(0, s.Length);
int safeStart = start.Fit(0, s.Length);
int safeLength = end - safeStart;
try
{
return s.Substring(safeStart, safeLength);
}
catch (Exception e)
{
CUI.Log($"SubstringSafe {e.Message}\ns:\"{s}\" start: {start}->{safeStart} end: {end} length: {length}->{safeLength} ", Color.Orange);
return "";
}
}
public static Dictionary<string, string> ParseKVPairs(string raw)
{
Dictionary<string, string> props = new();
if (raw == null || raw == "") return props;
string content = raw.Split('{', '}')[1];
List<string> expressions = new();
int start = 0;
int end = 0;
int depth = 0;
for (int i = 0; i < content.Length; i++)
{
char c = content[i];
end = i;
if (c == '[' || c == '{') depth++;
if (c == ']' || c == '}') depth--;
if (depth <= 0 && c == ',')
{
expressions.Add(content.Substring(start, end - start));
start = end + 1;
}
}
expressions.Add(content.Substring(start, end - start));
var pairs = expressions.Select(s => s.Split(':').Select(sub => sub.Trim()).ToArray());
foreach (var pair in pairs) { props[pair[0].ToLower()] = pair[1]; }
return props;
}
public static string ColorToString(Color c) => $"{c.R},{c.G},{c.B},{c.A}";
public static string Vector2ToString(Vector2 v) => $"[{v.X},{v.Y}]";
public static string NullVector2ToString(Vector2? v) => v.HasValue ? $"[{v.Value.X},{v.Value.Y}]" : "null";
public static string NullIntToString(int? i) => i.HasValue ? $"{i}" : "null";
public static string RectangleToString(Rectangle r) => $"[{r.X},{r.Y},{r.Width},{r.Height}]";
public static string GUIFontToString(GUIFont f) => f.Identifier.Value;
public static string SpriteEffectsToString(SpriteEffects e)
{
if ((int)e == 3) return "FlipBothSides";
else return e.ToString();
}
public static string IEnumerableStringToString(IEnumerable<string> e) => $"[{string.Join(',', e.ToArray())}]";
public static IEnumerable<string> ParseIEnumerableString(string raw)
{
if (raw == null || raw == "") return new List<string>();
string content = raw.Split('[', ']')[1];
return content.Split(',');
}
public static string ParseString(string s) => s; // BaroDev (wide)
//public static GUISoundType ParseGUISoundType(string s) => Enum.Parse<GUISoundType>(s);
public static GUIFont ParseGUIFont(string raw)
{
GUIFont font = GUIStyle.Fonts.GetValueOrDefault(new Identifier(raw.Trim()));
font ??= GUIStyle.Font;
return font;
}
public static SpriteEffects ParseSpriteEffects(string raw)
{
if (raw == "FlipBothSides") return SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically;
else return Enum.Parse<SpriteEffects>(raw);
}
public static int? ParseNullInt(string raw)
{
if (raw == "null") return null;
return int.Parse(raw);
}
public static Vector2? ParseNullVector2(string raw)
{
if (raw == "null") return null;
return ParseVector2(raw);
}
public static Vector2 ParseVector2(string raw)
{
if (raw == null || raw == "") return new Vector2(0, 0);
string content = raw.Split('[', ']')[1];
List<string> coords = content.Split(',').Select(s => s.Trim()).ToList();
float x = 0;
float y = 0;
float.TryParse(coords.ElementAtOrDefault(0), out x);
float.TryParse(coords.ElementAtOrDefault(1), out y);
return new Vector2(x, y);
}
public static Rectangle ParseRectangle(string raw)
{
if (raw == null || raw == "") return new Rectangle(0, 0, 1, 1);
string content = raw.Split('[', ']')[1];
List<string> coords = content.Split(',').Select(s => s.Trim()).ToList();
int x = 0;
int y = 0;
int w = 0;
int h = 0;
int.TryParse(coords.ElementAtOrDefault(0), out x);
int.TryParse(coords.ElementAtOrDefault(1), out y);
int.TryParse(coords.ElementAtOrDefault(2), out w);
int.TryParse(coords.ElementAtOrDefault(3), out h);
return new Rectangle(x, y, w, h);
}
public static Color ParseColor(string s) => XMLExtensions.ParseColor(s, false);
public static Dictionary<Type, MethodInfo> Parse;
public static Dictionary<Type, MethodInfo> CustomToString;
internal static void InitStatic()
{
CUI.OnInit += () =>
{
Stopwatch sw = Stopwatch.StartNew();
Parse = new Dictionary<Type, MethodInfo>();
CustomToString = new Dictionary<Type, MethodInfo>();
Parse[typeof(string)] = typeof(CUIExtensions).GetMethod("ParseString");
//Parse[typeof(GUISoundType)] = typeof(CUIExtensions).GetMethod("ParseGUISoundType");
Parse[typeof(Rectangle)] = typeof(CUIExtensions).GetMethod("ParseRectangle");
Parse[typeof(GUIFont)] = typeof(CUIExtensions).GetMethod("ParseGUIFont");
Parse[typeof(Vector2?)] = typeof(CUIExtensions).GetMethod("ParseNullVector2");
Parse[typeof(Vector2)] = typeof(CUIExtensions).GetMethod("ParseVector2");
Parse[typeof(SpriteEffects)] = typeof(CUIExtensions).GetMethod("ParseSpriteEffects");
Parse[typeof(Color)] = typeof(CUIExtensions).GetMethod("ParseColor");
Parse[typeof(int?)] = typeof(CUIExtensions).GetMethod("ParseNullInt");
Parse[typeof(IEnumerable<string>)] = typeof(CUIExtensions).GetMethod("ParseIEnumerableString");
CustomToString[typeof(IEnumerable<string>)] = typeof(CUIExtensions).GetMethod("IEnumerableStringToString");
CustomToString[typeof(int?)] = typeof(CUIExtensions).GetMethod("NullIntToString");
CustomToString[typeof(Color)] = typeof(CUIExtensions).GetMethod("ColorToString");
CustomToString[typeof(SpriteEffects)] = typeof(CUIExtensions).GetMethod("SpriteEffectsToString");
CustomToString[typeof(Vector2)] = typeof(CUIExtensions).GetMethod("Vector2ToString");
CustomToString[typeof(Vector2?)] = typeof(CUIExtensions).GetMethod("NullVector2ToString");
CustomToString[typeof(GUIFont)] = typeof(CUIExtensions).GetMethod("GUIFontToString");
CustomToString[typeof(Rectangle)] = typeof(CUIExtensions).GetMethod("RectangleToString");
CUIDebug.Log($"CUIExtensions.Initialize took {sw.ElapsedMilliseconds}ms");
};
CUI.OnDispose += () =>
{
Parse.Clear();
CustomToString.Clear();
};
}
}
}

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.IO;
using EventInput;
namespace QICrabUI
{
public class CUIFocusResolver
{
private CUIComponent focusedCUIComponent;
public CUIComponent FocusedCUIComponent
{
get => focusedCUIComponent;
set
{
CUIComponent oldFocused = focusedCUIComponent;
CUIComponent newFocused = value;
if (oldFocused == newFocused) return;
if (oldFocused != null)
{
oldFocused.Focused = false;
oldFocused.InvokeOnFocusLost();
}
if (newFocused != null)
{
newFocused.Focused = true;
newFocused.InvokeOnFocus();
}
if (oldFocused is IKeyboardSubscriber || newFocused is null)
{
OnVanillaIKeyboardSubscriberSet(null, true);
}
if (newFocused is IKeyboardSubscriber)
{
OnVanillaIKeyboardSubscriberSet((IKeyboardSubscriber)newFocused, true);
}
focusedCUIComponent = value;
}
}
public void OnVanillaIKeyboardSubscriberSet(IKeyboardSubscriber value, bool callFromCUI = false)
{
try
{
KeyboardDispatcher _ = GUI.KeyboardDispatcher;
IKeyboardSubscriber oldSubscriber = _._subscriber;
IKeyboardSubscriber newSubscriber = value;
if (newSubscriber == oldSubscriber) { return; }
// this case should be handled in CUI
if (!callFromCUI && oldSubscriber is CUIComponent && newSubscriber is null) { return; }
//CUI.Log($"new IKeyboardSubscriber {oldSubscriber} -> {newSubscriber}");
if (oldSubscriber != null)
{
TextInput.StopTextInput();
oldSubscriber.Selected = false;
}
if (oldSubscriber is CUIComponent component && newSubscriber is GUITextBox)
{
//TODO for some season TextInput doesn't loose focus here
component.InvokeOnFocusLost();
component.Focused = false;
focusedCUIComponent = null;
}
if (newSubscriber != null)
{
if (newSubscriber is GUITextBox box)
{
TextInput.SetTextInputRect(box.MouseRect);
TextInput.StartTextInput();
TextInput.SetTextInputRect(box.MouseRect);
}
if (newSubscriber is CUIComponent)
{
TextInput.StartTextInput();
}
newSubscriber.Selected = true;
}
_._subscriber = value;
}
catch (Exception e)
{
CUI.Error(e);
}
}
}
}

View File

@@ -0,0 +1,106 @@
#define USELUA
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using HarmonyLib;
using MoonSharp.Interpreter;
namespace QICrabUI
{
public class CUIInternalAttribute : System.Attribute { }
[CUIInternal]
public class CUILuaRegistrar
{
public static string CUITypesFile => Path.Combine(CUI.LuaFolder, "CUITypes.lua");
public static bool IsRealCUIType(Type T)
{
if (T.DeclaringType != null) return false; // nested type
if (T.Name == "<>c") return false; // guh
if (T.IsGenericType) return false; // in lua?
if (T.IsInterface) return false;
if (T.IsSubclassOf(typeof(Attribute))) return false;
if (Attribute.IsDefined(T, typeof(CUIInternalAttribute))) return false;
if (typeof(CUILuaRegistrar).Namespace != T.Namespace) return false;
return true;
}
#if !USELUA
[Conditional("DONT")]
#endif
public void Register()
{
if (CUI.LuaFolder == null) return;
if (!Directory.Exists(CUI.LuaFolder) || !CUI.UseLua) return;
Assembly thisAssembly = Assembly.GetAssembly(typeof(CUILuaRegistrar));
foreach (Type T in thisAssembly.GetTypes().Where(IsRealCUIType))
{
LuaUserData.RegisterType(T.FullName);
// This has to be done in lua
//GameMain.LuaCs.Lua.Globals[T.Name] = UserData.CreateStatic(T);
}
GameMain.LuaCs.RegisterAction<CUIInput>();
GameMain.LuaCs.RegisterAction<float, float>();
GameMain.LuaCs.RegisterAction<TextInputEventArgs>();
GameMain.LuaCs.RegisterAction<string>();
GameMain.LuaCs.RegisterAction<CUIComponent>();
GameMain.LuaCs.RegisterAction<bool>();
GameMain.LuaCs.RegisterAction<CUIComponent, int>();
LuaUserData.RegisterType(typeof(CUI).FullName);
GameMain.LuaCs.Lua.Globals[nameof(CUI)] = UserData.CreateStatic(typeof(CUI));
ConstructLuaStaticsFile();
}
#if !USELUA
[Conditional("DONT")]
#endif
public void Deregister()
{
try
{
GameMain.LuaCs.Lua.Globals[nameof(CUI)] = null;
}
catch (Exception e)
{
CUI.Error(e);
}
}
public void ConstructLuaStaticsFile()
{
if (!Directory.Exists(CUI.LuaFolder)) return;
Assembly thisAssembly = Assembly.GetAssembly(typeof(CUILuaRegistrar));
string content = "-- This file is autogenerated\n";
foreach (Type T in thisAssembly.GetTypes().Where(IsRealCUIType))
{
content += $"{T.Name} = LuaUserData.CreateStatic('{T.FullName}', true)\n";
}
using (StreamWriter writer = new StreamWriter(CUITypesFile, false))
{
writer.Write(content);
}
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System.IO;
namespace QICrabUI
{
public class CUIMultiModResolver
{
internal static void InitStatic()
{
CUI.OnInit += () =>
{
//FindOtherInputs();
};
CUI.OnDispose += () =>
{
CUIInputs.Clear();
CUIs.Clear();
MouseInputHandledMethods.Clear();
};
}
public static List<object> CUIInputs = new();
public static List<object> CUIs = new();
public static List<Action<bool>> MouseInputHandledMethods = new();
public static void MarkOtherInputsAsHandled()
{
//MouseInputHandledMethods.ForEach(action => action(true));
foreach (object input in CUIInputs)
{
try
{
PropertyInfo setAsHandled = input.GetType().GetProperty("MouseInputHandled");
setAsHandled.SetValue(input, true);
CUI.Log($"setAsHandled.SetValue(input, true) for {input}");
}
catch (Exception e)
{
CUI.Warning($"Couldn't find MouseInputHandled in CUIInput in CUI from other mod ({input.GetType()})");
continue;
}
}
}
public static void FindOtherInputs()
{
AppDomain currentDomain = AppDomain.CurrentDomain;
foreach (Assembly asm in currentDomain.GetAssemblies())
{
foreach (Type T in asm.GetTypes())
{
if (T.Name == "CUI")
{
try
{
FieldInfo InstanceField = T.GetField("Instance", BindingFlags.Static | BindingFlags.Public);
object CUIInstance = InstanceField.GetValue(null);
if (CUIInstance != null && CUIInstance != CUI.Instance)
{
CUIs.Add(CUIInstance);
FieldInfo inputField = T.GetField("input", AccessTools.all);
object input = inputField.GetValue(CUIInstance);
if (input != null) CUIInputs.Add(input);
}
}
catch (Exception e)
{
CUI.Warning($"Couldn't find CUIInputs in CUI from other mod ({T})");
continue;
}
}
}
}
foreach (object input in CUIInputs)
{
try
{
PropertyInfo setAsHandled = input.GetType().GetProperty("MouseInputHandled");
MouseInputHandledMethods.Add(setAsHandled.SetMethod.CreateDelegate<Action<bool>>(input));
}
catch (Exception e)
{
CUI.Warning($"Couldn't find MouseInputHandled in CUIInput in CUI from other mod ({input.GetType()})");
continue;
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More