item-info wip

item-info
HumanoidSandvichDispenser 2023-03-26 21:11:38 -07:00
parent ae5767e1fd
commit f0a33b0c77
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
26 changed files with 679 additions and 105 deletions

View File

@ -159,7 +159,8 @@ namespace SupaLidlGame.Characters
return; return;
} }
if (Inventory.SelectedItem is Weapon weapon) // TODO: support for offhand items
if (Inventory.PrimaryItem is Weapon weapon)
{ {
weapon.Use(); weapon.Use();
} }

View File

@ -7,7 +7,8 @@ namespace SupaLidlGame.Characters
{ {
public override void _Ready() public override void _Ready()
{ {
Inventory.SelectedItem = Inventory.GetNode<Items.Item>("Sword"); //Inventory.SelectedItem = Inventory.GetNode<Items.Item>("Sword");
Inventory.SelectFirstItem();
base._Ready(); base._Ready();
} }

View File

@ -165,7 +165,6 @@ y_sort_enabled = true
script = ExtResource("7_43gq8") script = ExtResource("7_43gq8")
[node name="Sword" parent="Inventory" instance=ExtResource("8_s3c8r")] [node name="Sword" parent="Inventory" instance=ExtResource("8_s3c8r")]
Knockback = 40.0
[node name="FlashAnimation" type="AnimationPlayer" parent="."] [node name="FlashAnimation" type="AnimationPlayer" parent="."]
libraries = { libraries = {

View File

@ -288,7 +288,8 @@ namespace SupaLidlGame.Characters
if (Target.LengthSquared() < 1024) if (Target.LengthSquared() < 1024)
{ {
if (Inventory.SelectedItem is Weapon weapon) // TODO: offhand items
if (Inventory.PrimaryItem is Weapon weapon)
{ {
UseCurrentItem(); UseCurrentItem();
} }

View File

@ -61,5 +61,19 @@ namespace SupaLidlGame.Characters
GD.Print("died"); GD.Print("died");
//base.Die(); //base.Die();
} }
public bool IsUsingAnyWeapon()
{
bool checkItem(Items.Item item)
{
if (item is Items.Weapon weapon)
{
return weapon.IsUsing;
}
return false;
}
return checkItem(Inventory.PrimaryItem) ||
checkItem(Inventory.OffhandItem);
}
} }
} }

View File

@ -8,7 +8,6 @@
[ext_resource type="PackedScene" uid="uid://cl56eadpklnbo" path="res://Utils/PlayerCamera.tscn" id="4_ym125"] [ext_resource type="PackedScene" uid="uid://cl56eadpklnbo" path="res://Utils/PlayerCamera.tscn" id="4_ym125"]
[ext_resource type="Script" path="res://Characters/States/PlayerMoveState.cs" id="5_tx5rw"] [ext_resource type="Script" path="res://Characters/States/PlayerMoveState.cs" id="5_tx5rw"]
[ext_resource type="Script" path="res://Characters/States/PlayerRollState.cs" id="6_6bgrj"] [ext_resource type="Script" path="res://Characters/States/PlayerRollState.cs" id="6_6bgrj"]
[ext_resource type="PackedScene" uid="uid://d72ehtv1ks0e" path="res://Items/Weapons/Sword.tscn" id="7_4rxuv"]
[ext_resource type="Script" path="res://Items/Inventory.cs" id="7_xyenu"] [ext_resource type="Script" path="res://Items/Inventory.cs" id="7_xyenu"]
[ext_resource type="PackedScene" uid="uid://cjgxyhgcyvsv7" path="res://BoundingBoxes/Hurtbox.tscn" id="9_avyu4"] [ext_resource type="PackedScene" uid="uid://cjgxyhgcyvsv7" path="res://BoundingBoxes/Hurtbox.tscn" id="9_avyu4"]
[ext_resource type="AudioStream" uid="uid://njun3e6v4854" path="res://Assets/Sounds/hurt.wav" id="12_h0x0g"] [ext_resource type="AudioStream" uid="uid://njun3e6v4854" path="res://Assets/Sounds/hurt.wav" id="12_h0x0g"]
@ -86,6 +85,69 @@ size = Vector2(16, 8)
[sub_resource type="LabelSettings" id="LabelSettings_q5h1n"] [sub_resource type="LabelSettings" id="LabelSettings_q5h1n"]
font_size = 24 font_size = 24
[sub_resource type="CSharpScript" id="CSharpScript_rcje3"]
script/source = "using Godot;
using SupaLidlGame.Characters;
namespace SupaLidlGame.Items
{
public partial class ItemInfo : Resource
{
[Export]
public string ItemName { get; set; } = \"Item Name\";
[Export]
public string Description { get; set; } = \"Item description.\";
[Export]
public bool CanStack { get; set; } = false;
[Export]
public int Count { get; set; } = 1;
[Export]
public bool IsOneHanded { get; set; } = false;
[Export]
public Character CharacterOwner { get; set; } = null;
[Export]
public Texture2D Texture { get; set; }
[Export]
public string ScenePath { get; set; }
/// <summary>
/// Determines if this item can directly stack with other items
/// </summary>
public virtual bool StacksWith(ItemInfo itemInfo)
{
if (!CanStack)
{
return false;
}
if (ItemName != itemInfo.ItemName)
{
return false;
}
// several more conditions may be added soon
return true;
}
public Item InstantiateItem(string name = \"Primary\")
{
var scene = ResourceLoader.Load<PackedScene>(ScenePath);
var instance = scene.Instantiate<Item>();
instance.Name = name;
return instance;
}
}
}
"
[sub_resource type="RectangleShape2D" id="RectangleShape2D_cjk6b"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_cjk6b"]
size = Vector2(16, 24) size = Vector2(16, 24)
@ -193,8 +255,7 @@ horizontal_alignment = 1
y_sort_enabled = true y_sort_enabled = true
position = Vector2(0, 2) position = Vector2(0, 2)
script = ExtResource("7_xyenu") script = ExtResource("7_xyenu")
Items = Array[Resource]([SubResource("CSharpScript_rcje3")])
[node name="Sword" parent="Inventory" instance=ExtResource("7_4rxuv")]
[node name="Hurtbox" parent="." instance=ExtResource("9_avyu4")] [node name="Hurtbox" parent="." instance=ExtResource("9_avyu4")]
Faction = 1 Faction = 1

View File

@ -9,11 +9,11 @@ namespace SupaLidlGame.Characters.State
public override CharacterState Enter(CharacterState previousState) public override CharacterState Enter(CharacterState previousState)
{ {
if (Character.Inventory.SelectedItem is Weapon weapon) if (Character.Inventory.PrimaryItem is Weapon weapon)
{ {
_attackTime = weapon.UseTime; _attackTime = weapon.Info.UseTime;
weapon.Visible = true; weapon.Visible = true;
Character.Inventory.SelectedItem.Use(); Character.Inventory.PrimaryItem.Use();
} }
else else
{ {
@ -24,12 +24,12 @@ namespace SupaLidlGame.Characters.State
public override void Exit(CharacterState nextState) public override void Exit(CharacterState nextState)
{ {
if (Character.Inventory.SelectedItem is null) if (Character.Inventory.PrimaryItem is null)
{ {
} }
Character.Inventory.SelectedItem.Deuse(); Character.Inventory.PrimaryItem.Deuse();
if (Character.Inventory.SelectedItem is Weapon weapon) if (Character.Inventory.PrimaryItem is Weapon weapon)
{ {
//weapon.Visible = false; //weapon.Visible = false;
} }

View File

@ -29,14 +29,7 @@ namespace SupaLidlGame.Characters.State
{ {
if (@event.IsActionPressed("roll")) if (@event.IsActionPressed("roll"))
{ {
if (Character.Inventory.SelectedItem is Weapon weapon) if (!_player.IsUsingAnyWeapon())
{
if (!weapon.IsUsing)
{
return RollState;
}
}
else
{ {
return RollState; return RollState;
} }

View File

@ -16,7 +16,9 @@ namespace SupaLidlGame.Characters.State
#if DEBUG #if DEBUG
if (@event.IsActionPressed("equip")) if (@event.IsActionPressed("equip"))
{ {
Character.Inventory.SelectedItem = Character.Inventory.GetNode<Items.Item>("Sword"); //Character.Inventory.SelectedItem = Character.Inventory.GetNode<Items.Item>("Sword");
//Character.Inventory.SelectedItem = Character.
Character.Inventory.SelectFirstItem();
} }
#endif #endif
@ -29,7 +31,7 @@ namespace SupaLidlGame.Characters.State
"ui_up", "ui_down"); "ui_up", "ui_down");
Vector2 mousePos = Character.GetGlobalMousePosition(); Vector2 mousePos = Character.GetGlobalMousePosition();
Vector2 dirToMouse = Character.GlobalPosition.DirectionTo(mousePos); Vector2 dirToMouse = Character.GlobalPosition.DirectionTo(mousePos);
if (Character.Inventory.SelectedItem is Weapon weapon) if (Character.Inventory.PrimaryItem is Weapon weapon)
{ {
if (!weapon.IsUsing) if (!weapon.IsUsing)
{ {
@ -39,7 +41,7 @@ namespace SupaLidlGame.Characters.State
if (Godot.Input.IsActionPressed("attack1")) if (Godot.Input.IsActionPressed("attack1"))
{ {
if (Character.Inventory.SelectedItem is not null) if (Character.Inventory.PrimaryItem is not null)
{ {
Character.UseCurrentItem(); Character.UseCurrentItem();
} }

View File

@ -0,0 +1,12 @@
using Godot;
namespace SupaLidlGame.Extensions
{
public static class ResourceExtensions
{
public static string GetFileName(this Resource resource)
{
return resource.ResourcePath.GetFile();
}
}
}

View File

@ -8,39 +8,96 @@ namespace SupaLidlGame.Items
{ {
public Character Character { get; private set; } public Character Character { get; private set; }
public List<Item> Items { get; private set; } = new List<Item>(); [Export]
public Godot.Collections.Array<ItemInfo> Items { get; private set; }
[Export]
public ItemInfo Test { get; set; }
public const int MaxCapacity = 32; public const int MaxCapacity = 32;
private Item _selectedItem; private Item _primaryItem;
private Item _offhandItem; private Item _offhandItem;
public Item SelectedItem public Item PrimaryItem
{ {
get => _selectedItem; get => _primaryItem;
set => EquipItem(value, ref _selectedItem); private set => _primaryItem = value;
} }
public Item OffhandItem public Item OffhandItem
{ {
get => _selectedItem; get => _offhandItem;
set => EquipItem(value, ref _offhandItem); private set => _offhandItem = value;
} }
private bool EquipItem(Item item, ref Item slot) public Inventory()
{ {
if (item is not null && item.IsOneHanded) Items = new Godot.Collections.Array<ItemInfo>();
{ }
// we can not equip this if either hand is occupied by
// two-handed item
if (_selectedItem is not null && !_selectedItem.IsOneHanded) public bool EquipItem(ItemInfo info, bool offhand = false)
{
if (UnequipItem() == info)
{
// if the item that was unequipped was our item, then we stop
return true;
}
var idx = Items.IndexOf(info);
if (idx < 0)
{ {
return false; return false;
} }
if (_offhandItem is not null && !_offhandItem.IsOneHanded) ItemInfo item = Items[idx];
string name = offhand ? "Offhand" : "Primary";
PrimaryItem = item.InstantiateItem(name);
PrimaryItem.Equip(Character);
PrimaryItem.CharacterOwner = Character;
//PrimaryItem.CharacterOwner
AddChild(PrimaryItem);
return true;
}
public ItemInfo UnequipItem(bool offhand = false)
{
Item item;
if (!offhand)
{
item = Character.Inventory.GetNode<Item>("Primary");
}
else
{
item = Character.Inventory.GetNode<Item>("Offhand");
}
if (item is null)
{
return null;
}
item.QueueFree();
return item.Info;
}
/*
private bool EquipItem(Item item, ref Item slot)
{
if (item is not null && item.Info.IsOneHanded)
{
// we can not equip this if either hand is occupied by
// two-handed item
if (_selectedItem is not null && !_selectedItem.Info.IsOneHanded)
{
return false;
}
if (_offhandItem is not null && !_offhandItem.Info.IsOneHanded)
{ {
return false; return false;
} }
@ -66,39 +123,59 @@ namespace SupaLidlGame.Items
return true; return true;
} }
*/
public Item AddItem(Item item) public ItemInfo AddItem(ItemInfo info)
{ {
if (Items.Count >= MaxCapacity) if (Items.Count >= MaxCapacity)
{ {
return null; return null;
} }
item.CharacterOwner = Character; info.CharacterOwner = Character;
item.Visible = false; //item.Visible = false;
Items.Add(item); Items.Add(info);
return item; return info;
} }
public Item DropItem(Item item) public Item DropItem(ItemInfo item)
{ {
item.CharacterOwner = null; item.CharacterOwner = null;
item.Visible = true; //item.Visible = true;
var e = SelectedItem = item; //var e = SelectedItem = item;
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }
public override void _Ready() public override void _Ready()
{ {
Character = GetParent<Character>(); Character = GetParent<Character>();
EnsureChildrenItems();
base._Ready();
}
private void EnsureChildrenItems()
{
foreach (Node child in GetChildren()) foreach (Node child in GetChildren())
{ {
if (child is Item item) if (child is Item item)
{ {
AddItem(item); GD.Print(item.Info.Count);
if (Items.IndexOf(item.Info) < 0)
{
AddItem(item.Info);
PrimaryItem = item;
} }
} }
base._Ready(); }
}
public void SelectFirstItem()
{
EnsureChildrenItems();
if (Items.Count > 0)
{
EquipItem(Items[0]);
}
} }
} }
} }

View File

@ -6,17 +6,7 @@ namespace SupaLidlGame.Items
public abstract partial class Item : Node2D public abstract partial class Item : Node2D
{ {
[Export] [Export]
public string ItemName { get; set; } public ItemInfo Info { get; set; }
[Export]
public string Description { get; set; }
[Export]
public bool CanStack { get; set; } = false;
public int Count { get; set; } = 1;
public bool IsOneHanded { get; set; } = false;
public Character CharacterOwner { get; set; } public Character CharacterOwner { get; set; }
@ -25,12 +15,12 @@ namespace SupaLidlGame.Items
/// </summary> /// </summary>
public virtual bool StacksWith(Item item) public virtual bool StacksWith(Item item)
{ {
if (!CanStack) if (!Info.CanStack)
{ {
return false; return false;
} }
if (ItemName != item.ItemName) if (Info.ItemName != item.Info.ItemName)
{ {
return false; return false;
} }

View File

@ -0,0 +1,7 @@
namespace SupaLidlGame
{
public class ItemDatabase
{
}
}

63
Items/ItemInfo.cs 100644
View File

@ -0,0 +1,63 @@
using Godot;
using SupaLidlGame.Characters;
using MonoCustomResourceRegistry;
namespace SupaLidlGame.Items
{
[RegisteredType(nameof(ItemInfo))]
public partial class ItemInfo : Resource
{
[Export]
public string ItemName { get; set; } = "Item Name";
[Export]
public string Description { get; set; } = "Item description.";
[Export]
public bool CanStack { get; set; } = false;
[Export]
public int Count { get; set; } = 1;
[Export]
public bool IsOneHanded { get; set; } = false;
[Export]
public Character CharacterOwner { get; set; } = null;
[Export]
public Texture2D Texture { get; set; }
[Export]
public string ScenePath { get; set; }
/// <summary>
/// Determines if this item can directly stack with other items
/// </summary>
public virtual bool StacksWith(ItemInfo itemInfo)
{
if (!CanStack)
{
return false;
}
if (ItemName != itemInfo.ItemName)
{
return false;
}
// several more conditions may be added soon
return true;
}
public Item InstantiateItem(string name = "Primary")
{
var scene = ResourceLoader.Load<PackedScene>(ScenePath);
var instance = scene.Instantiate<Item>();
instance.Name = name;
instance.Info = this;
return instance;
}
}
}

17
Items/MeleeInfo.cs 100644
View File

@ -0,0 +1,17 @@
using Godot;
namespace SupaLidlGame.Items.Weapons
{
public partial class MeleeInfo : WeaponInfo
{
/// <summary>
/// The time frame in seconds for which the weapon will deal damage.
/// </summary>
/// <remarks>
/// The value of <c>AttackTime</c> should be less than the
/// value of <c>UseTime</c>
/// </remarks>
[Export]
public double AttackTime { get; set; } = 0;
}
}

View File

@ -10,30 +10,7 @@ namespace SupaLidlGame.Items
public bool IsUsing => RemainingUseTime > 0; public bool IsUsing => RemainingUseTime > 0;
/// <summary> public new WeaponInfo Info => base.Info as WeaponInfo;
/// How much damage in HP that this weapon deals.
/// </summary>
[Export]
public float Damage { get; set; } = 0;
/// <summary>
/// The time in seconds it takes for this weapon to become available
/// again after using.
/// </summary>
[Export]
public double UseTime { get; set; } = 0;
/// <summary>
/// The magnitude of the knockback force of the weapon.
/// </summary>
[Export]
public float Knockback { get; set; } = 0;
/// <summary>
/// The initial velocity of any projectile the weapon may spawn.
/// </summary>
[Export]
public float InitialVelocity { get; set; } = 0;
/// <summary> /// <summary>
/// Whether or not the weapon can parry other weapons and is /// Whether or not the weapon can parry other weapons and is
@ -51,6 +28,7 @@ namespace SupaLidlGame.Items
public override void Equip(Character character) public override void Equip(Character character)
{ {
GD.Print("Equipped by " + character.Name);
Character = character; Character = character;
} }
@ -61,7 +39,7 @@ namespace SupaLidlGame.Items
public override void Use() public override void Use()
{ {
RemainingUseTime = UseTime; RemainingUseTime = Info.UseTime;
} }
public override void Deuse() public override void Deuse()
@ -84,7 +62,7 @@ namespace SupaLidlGame.Items
{ {
if (box is Hurtbox hurtbox) if (box is Hurtbox hurtbox)
{ {
hurtbox.InflictDamage(Damage, Character, Knockback); hurtbox.InflictDamage(Info.Damage, Character, Info.Knockback);
} }
} }
} }

View File

@ -0,0 +1,33 @@
using Godot;
namespace SupaLidlGame.Items
{
public partial class WeaponInfo : ItemInfo
{
/// <summary>
/// How much damage in HP that this weapon deals.
/// </summary>
[Export]
public float Damage { get; set; } = 0;
/// <summary>
/// The time in seconds it takes for this weapon to become available
/// again after using.
/// </summary>
[Export]
public double UseTime { get; set; } = 0;
/// <summary>
/// The magnitude of the knockback force of the weapon.
/// </summary>
[Export]
public float Knockback { get; set; } = 0;
/// <summary>
/// The initial velocity of any projectile the weapon may spawn.
/// </summary>
[Export]
public float InitialVelocity { get; set; } = 0;
}
}

View File

@ -15,16 +15,6 @@ namespace SupaLidlGame.Items.Weapons
[Export] [Export]
public AnimationPlayer AnimationPlayer { get; set; } public AnimationPlayer AnimationPlayer { get; set; }
/// <summary>
/// The time frame in seconds for which the weapon will deal damage.
/// </summary>
/// <remarks>
/// The value of <c>AttackTime</c> should be less than the
/// value of <c>UseTime</c>
/// </remarks>
[Export]
public double AttackTime { get; set; } = 0;
[Export] [Export]
public CpuParticles2D ParryParticles { get; set; } public CpuParticles2D ParryParticles { get; set; }
@ -103,7 +93,7 @@ namespace SupaLidlGame.Items.Weapons
public override void _Ready() public override void _Ready()
{ {
Hitbox.Damage = Damage; Hitbox.Damage = Info.Damage;
} }
public override void _Process(double delta) public override void _Process(double delta)
@ -123,7 +113,10 @@ namespace SupaLidlGame.Items.Weapons
GD.Print("processing hit"); GD.Print("processing hit");
if (box is Hurtbox hurtbox) if (box is Hurtbox hurtbox)
{ {
hurtbox.InflictDamage(Damage, Character, Knockback); hurtbox.InflictDamage(
Info.Damage,
Character,
Info.Knockback);
} }
} }
} }
@ -165,10 +158,13 @@ namespace SupaLidlGame.Items.Weapons
{ {
if (hurt.GetParent() is Character c) if (hurt.GetParent() is Character c)
{ {
var item = c.Inventory.SelectedItem; if (c.Inventory.PrimaryItem is Weapon primary)
if (item is Weapon w)
{ {
AttemptParry(w); AttemptParry(primary);
}
if (c.Inventory.OffhandItem is Weapon offhand)
{
AttemptParry(offhand);
} }
} }
} }

View File

@ -0,0 +1,17 @@
[gd_resource type="Resource" load_steps=2 format=3 uid="uid://dbqk11m54e72e"]
[ext_resource type="Script" path="res://Items/MeleeInfo.cs" id="1_w5yvw"]
[resource]
script = ExtResource("1_w5yvw")
AttackTime = 0.1
Damage = 20.0
UseTime = 0.8
Knockback = 80.0
InitialVelocity = 0.0
ItemName = "Sword"
Description = "A basic sword."
CanStack = false
Count = 1
IsOneHanded = false
ScenePath = ""

View File

@ -1,6 +1,7 @@
[gd_scene load_steps=20 format=3 uid="uid://d72ehtv1ks0e"] [gd_scene load_steps=21 format=3 uid="uid://d72ehtv1ks0e"]
[ext_resource type="Script" path="res://Items/Weapons/Sword.cs" id="1_mlo73"] [ext_resource type="Script" path="res://Items/Weapons/Sword.cs" id="1_mlo73"]
[ext_resource type="Resource" uid="uid://dbqk11m54e72e" path="res://Items/Weapons/Sword.tres" id="2_5vlf6"]
[ext_resource type="Texture2D" uid="uid://dt6u8p4h6g7le" path="res://Assets/Sprites/knife.png" id="2_rnfo4"] [ext_resource type="Texture2D" uid="uid://dt6u8p4h6g7le" path="res://Assets/Sprites/knife.png" id="2_rnfo4"]
[ext_resource type="PackedScene" uid="uid://du5vhccg75nrq" path="res://BoundingBoxes/Hitbox.tscn" id="3_up3ob"] [ext_resource type="PackedScene" uid="uid://du5vhccg75nrq" path="res://BoundingBoxes/Hitbox.tscn" id="3_up3ob"]
[ext_resource type="PackedScene" uid="uid://cojxmcin13ihm" path="res://Utils/Trail.tscn" id="4_pt6lq"] [ext_resource type="PackedScene" uid="uid://cojxmcin13ihm" path="res://Utils/Trail.tscn" id="4_pt6lq"]
@ -302,13 +303,8 @@ position = Vector2(2, 0)
script = ExtResource("1_mlo73") script = ExtResource("1_mlo73")
Hitbox = NodePath("Hitbox") Hitbox = NodePath("Hitbox")
AnimationPlayer = NodePath("AnimationPlayer") AnimationPlayer = NodePath("AnimationPlayer")
AttackTime = 0.1
ParryParticles = NodePath("Anchor/Sprite2D/ParryParticles") ParryParticles = NodePath("Anchor/Sprite2D/ParryParticles")
Damage = 20.0 Info = ExtResource("2_5vlf6")
UseTime = 0.8
Knockback = 80.0
ItemName = "Sword"
Description = "A basic sword."
[node name="Anchor" type="Node2D" parent="."] [node name="Anchor" type="Node2D" parent="."]
y_sort_enabled = true y_sort_enabled = true

View File

@ -0,0 +1,14 @@
using Godot;
namespace SupaLidlGame.Utils
{
public class AudioBuilder
{
private AudioStreamPlayer2D _audio;
public AudioBuilder(AudioStreamPlayer2D baseAudio)
{
_audio = baseAudio;
}
}
}

View File

@ -0,0 +1,208 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
// Originally written by wmigor
// Edited by Atlinx to recursively search for files.
// Edited by bls220 to update for Godot 4.0
// wmigor's Public Repo: https://github.com/wmigor/godot-mono-custom-resource-register
namespace MonoCustomResourceRegistry
{
#if TOOLS
[Tool]
public partial class Plugin : EditorPlugin
{
// We're not going to hijack the Mono Build button since it actually takes time to build
// and we can't be sure how long that is. I guess we have to leave refreshing to the user for now.
// There isn't any automation we can do to fix that.
// private Button MonoBuildButton => GetNode<Button>("/root/EditorNode/@@580/@@581/@@589/@@590/Button");
private readonly List<string> customTypes = new List<string>();
private Button? refreshButton;
public override void _EnterTree()
{
refreshButton = new Button();
refreshButton.Text = "CCR";
AddControlToContainer(CustomControlContainer.Toolbar, refreshButton);
refreshButton.Icon = GetEditorInterface().GetBaseControl().GetThemeIcon("Reload", "EditorIcons");
refreshButton.Pressed += OnRefreshPressed;
Settings.Init();
RefreshCustomClasses();
GD.PushWarning("You may change any setting for MonoCustomResourceRegistry in Project -> ProjectSettings -> General -> MonoCustomResourceRegistry");
}
public override void _ExitTree()
{
UnregisterCustomClasses();
RemoveControlFromContainer(CustomControlContainer.Toolbar, refreshButton);
refreshButton?.QueueFree();
}
public void RefreshCustomClasses()
{
GD.Print("\nRefreshing Registered Resources...");
UnregisterCustomClasses();
RegisterCustomClasses();
}
private void RegisterCustomClasses()
{
customTypes.Clear();
foreach (Type type in GetCustomRegisteredTypes())
if (type.IsSubclassOf(typeof(Resource)))
AddRegisteredType(type, nameof(Resource));
else
AddRegisteredType(type, nameof(Node));
}
private void AddRegisteredType(Type type, string defaultBaseTypeName)
{
RegisteredTypeAttribute? attribute = Attribute.GetCustomAttribute(type, typeof(RegisteredTypeAttribute)) as RegisteredTypeAttribute;
string? path = FindClassPath(type);
if (path == null && !FileAccess.FileExists(path))
return;
Script script = GD.Load<Script>(path);
if (script == null)
return;
string baseType = defaultBaseTypeName;
if (attribute is not null && attribute.baseType != "")
baseType = attribute.baseType;
ImageTexture? icon = null;
if (attribute is not null && attribute.iconPath != "")
{
if (FileAccess.FileExists(attribute.iconPath))
{
Texture2D rawIcon = ResourceLoader.Load<Texture2D>(attribute.iconPath);
if (rawIcon != null)
{
Image image = rawIcon.GetImage();
int length = (int)Mathf.Round(16 * GetEditorInterface().GetEditorScale());
image.Resize(length, length);
icon = ImageTexture.CreateFromImage(image);
}
else
GD.PushError($"Could not load the icon for the registered type \"{type.FullName}\" at path \"{path}\".");
}
else
GD.PushError($"The icon path of \"{path}\" for the registered type \"{type.FullName}\" does not exist.");
}
AddCustomType($"{Settings.ClassPrefix}{type.Name}", baseType, script, icon);
customTypes.Add($"{Settings.ClassPrefix}{type.Name}");
GD.Print($"Registered custom type: {type.Name} -> {path}");
}
private static string? FindClassPath(Type type)
{
switch (Settings.SearchType)
{
case Settings.ResourceSearchType.Recursive:
return FindClassPathRecursive(type);
case Settings.ResourceSearchType.Namespace:
return FindClassPathNamespace(type);
default:
throw new Exception($"ResourceSearchType {Settings.SearchType} not implemented!");
}
}
private static string? FindClassPathNamespace(Type type)
{
foreach (string dir in Settings.ResourceScriptDirectories)
{
StringBuilder builder = new(dir);
if (!dir.EndsWith('/'))
{
builder.Append('/');
}
if (type.Namespace is not null)
{
builder
.Append(type.Namespace.Replace(".", "/"))
.Append('/');
}
builder
.Append(type.Name)
.Append(".cs");
string filePath = builder.ToString();
if (FileAccess.FileExists(filePath))
return filePath;
}
return null;
}
private static string? FindClassPathRecursive(Type type)
{
foreach (string directory in Settings.ResourceScriptDirectories)
{
string? fileFound = FindClassPathRecursiveHelper(type, directory);
if (fileFound != null)
return fileFound;
}
return null;
}
private static string? FindClassPathRecursiveHelper(Type type, string directory)
{
var dir = DirAccess.Open(directory);
if (DirAccess.GetOpenError() == Error.Ok)
{
dir.ListDirBegin();
while (true)
{
var fileOrDirName = dir.GetNext();
// Skips hidden files like .
if (fileOrDirName == "")
break;
else if (fileOrDirName.StartsWith("."))
continue;
else if (dir.CurrentIsDir())
{
string? foundFilePath = FindClassPathRecursiveHelper(type, dir.GetCurrentDir() + "/" + fileOrDirName);
if (foundFilePath != null)
{
dir.ListDirEnd();
return foundFilePath;
}
}
else if (fileOrDirName == $"{type.Name}.cs")
return dir.GetCurrentDir() + "/" + fileOrDirName;
}
}
return null;
}
private static IEnumerable<Type> GetCustomRegisteredTypes()
{
var assembly = Assembly.GetAssembly(typeof(Plugin));
return assembly?.GetTypes().Where(t => !t.IsAbstract
&& Attribute.IsDefined(t, typeof(RegisteredTypeAttribute))
&& (t.IsSubclassOf(typeof(Node)) || t.IsSubclassOf(typeof(Resource)))
) ?? Enumerable.Empty<Type>();
}
private void UnregisterCustomClasses()
{
foreach (var script in customTypes)
{
RemoveCustomType(script);
GD.Print($"Unregister custom resource: {script}");
}
customTypes.Clear();
}
private void OnRefreshPressed()
{
RefreshCustomClasses();
}
}
#endif
}

View File

@ -0,0 +1,20 @@
using Godot;
using System;
namespace MonoCustomResourceRegistry
{
[AttributeUsage(System.AttributeTargets.Class)]
public partial class RegisteredTypeAttribute : System.Attribute
{
public string name;
public string iconPath;
public string baseType;
public RegisteredTypeAttribute(string name, string iconPath = "", string baseType = "")
{
this.name = name;
this.iconPath = iconPath;
this.baseType = baseType;
}
}
}

View File

@ -0,0 +1,57 @@
using Godot;
using Godot.Collections;
using System.Collections.ObjectModel;
using System.Linq;
namespace MonoCustomResourceRegistry
{
public static class Settings
{
public enum ResourceSearchType
{
Recursive = 0,
Namespace = 1,
}
public static string ClassPrefix => GetSettings(nameof(ClassPrefix)).AsString();
public static ResourceSearchType SearchType => (ResourceSearchType)GetSettings(nameof(SearchType)).AsInt32();
public static ReadOnlyCollection<string> ResourceScriptDirectories
{
get
{
Array array = (Array)GetSettings(nameof(ResourceScriptDirectories)) ?? new Array();
return new(array.Select(v => v.AsString()).ToList());
}
}
public static void Init()
{
AddSetting(nameof(ClassPrefix), Variant.Type.String, "");
AddSetting(nameof(SearchType), Variant.Type.Int, ResourceSearchType.Recursive, PropertyHint.Enum, "Recursive,Namespace");
AddSetting(nameof(ResourceScriptDirectories), Variant.Type.Array, new Array<string>(new string[] { "res://" }));
}
private static Variant GetSettings(string title)
{
return ProjectSettings.GetSetting($"{nameof(MonoCustomResourceRegistry)}/{title}");
}
private static void AddSetting<T>(string title, Variant.Type type, T value, PropertyHint hint = PropertyHint.None, string hintString = "")
{
title = SettingPath(title);
if (!ProjectSettings.HasSetting(title))
ProjectSettings.SetSetting(title, Variant.From(value));
var info = new Dictionary
{
["name"] = title,
["type"] = Variant.From(type),
["hint"] = Variant.From(hint),
["hint_string"] = hintString,
};
ProjectSettings.AddPropertyInfo(info);
GD.Print("Successfully added property: " + title);
}
private static string SettingPath(string title) => $"{nameof(MonoCustomResourceRegistry)}/{title}";
}
}

View File

@ -0,0 +1,7 @@
[plugin]
name="Mono Custom Resource Registry"
description="Registers custom C# resources for Godot"
author="Atlinx, wmigor, rob-mur"
version="3.0.0"
script="Plugin.cs"

View File

@ -8,6 +8,12 @@
config_version=5 config_version=5
[MonoCustomResourceRegistry]
ClassPrefix=""
SearchType=0
ResourceScriptDirectories=["res://"]
[application] [application]
config/name="SupaLidlGame" config/name="SupaLidlGame"
@ -19,6 +25,10 @@ config/icon="res://icon.svg"
project/assembly_name="SupaLidlGame" project/assembly_name="SupaLidlGame"
[editor_plugins]
enabled=PackedStringArray("res://addons/MonoCustomResourceRegistry/plugin.cfg")
[input] [input]
ui_left={ ui_left={