forked from Shardstone/trail-into-darkness
Added a bunch of utilities and modfief the character data structue
This commit is contained in:
@@ -100,7 +100,7 @@ namespace Nox.Core {
|
||||
|
||||
var adventureData = new AdventureData();
|
||||
|
||||
var characterBaseSettings = Addressables.LoadAssetAsync<CharacterBaseSettings>("CharacterBaseSettings").WaitForCompletion();
|
||||
var characterBaseSettings = Addressables.LoadAssetAsync<StarterCharacterSettings>("CharacterBaseSettings").WaitForCompletion();
|
||||
var perKRegistry = Addressables.LoadAssetAsync<PerksRegistry>("PerksRegistry ").WaitForCompletion();
|
||||
var characterRegistry = Addressables.LoadAssetAsync<CharacterRegistry>("CharacterRegistry").WaitForCompletion();
|
||||
var defaultPartySettings = Addressables.LoadAssetAsync<DefaultPartySettings>("DefaultPartySettings").WaitForCompletion();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface ICharacterAttributesFactory {
|
||||
EntityAttributes Create(EntityAttributes attributes);
|
||||
EntityAttributes Create(EntityAttributes entityAttributes);
|
||||
}
|
||||
|
||||
public sealed class CharacterAttributesFactory : ICharacterAttributesFactory {
|
||||
@@ -12,12 +13,12 @@ namespace Nox.Game {
|
||||
this.characterRegistry = characterRegistry;
|
||||
}
|
||||
|
||||
public EntityAttributes Create(EntityAttributes attributes) {
|
||||
if(attributes.might <= 0 || attributes.reflex <= 0 || attributes.knowledge <= 0 || attributes.perception <= 0) {
|
||||
public EntityAttributes Create(EntityAttributes entityAttributes) {
|
||||
if(entityAttributes.attributes.All(a => a.value != 0)) {
|
||||
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
|
||||
}
|
||||
|
||||
return attributes;
|
||||
//TODO: Handle attributes modifiers and perks
|
||||
return entityAttributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Nox.Game {
|
||||
public EntityStats stats;
|
||||
public EntityAttributes attributes;
|
||||
public PerksData perksData = new();
|
||||
public ModifiersData modifiersData = new();
|
||||
}
|
||||
|
||||
public sealed class CharacterFactory : ICharacterFactory {
|
||||
@@ -49,6 +50,7 @@ namespace Nox.Game {
|
||||
}
|
||||
|
||||
var attributes = attributesFactory.Create(request.attributes);
|
||||
var stats = statsFactory.Create(request.attributes);
|
||||
|
||||
var character = new CharacterDefinition {
|
||||
ID = string.IsNullOrWhiteSpace(request.id) ? Guid.NewGuid().ToString("N") : request.id,
|
||||
@@ -57,14 +59,9 @@ namespace Nox.Game {
|
||||
@class = request.characterClass,
|
||||
role = CharacterRole.Protagonist,
|
||||
Attributes = attributes,
|
||||
Stats = {
|
||||
level = statsFactory.Create(attributes).level,
|
||||
health = statsFactory.Create(attributes).health,
|
||||
stamina = statsFactory.Create(attributes).stamina,
|
||||
experience = statsFactory.Create(attributes).experience
|
||||
},
|
||||
perksData = new PerksData(),
|
||||
activeModifiers = new ModifiersData()
|
||||
Stats = stats,
|
||||
perksData = request.perksData ?? new PerksData(),
|
||||
activeModifiers = request.modifiersData ?? new ModifiersData()
|
||||
};
|
||||
|
||||
AddStartingPerks(character, request.perksData);
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface ICharacterStatsFactory {
|
||||
EntityStats Create(EntityAttributes attributes);
|
||||
}
|
||||
|
||||
public sealed class CharacterStatsFactoryOptions {
|
||||
public EntityStats entityStats = new EntityStats() {
|
||||
health = 10,
|
||||
stamina = 10,
|
||||
level = 1,
|
||||
experience = 0
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class CharacterStatsFactory : ICharacterStatsFactory {
|
||||
private readonly CharacterStatsFactoryOptions options;
|
||||
private readonly CharacterRegistry characterRegistry;
|
||||
|
||||
public CharacterStatsFactory(CharacterStatsFactoryOptions options = null) {
|
||||
this.options = options ?? new CharacterStatsFactoryOptions();
|
||||
public CharacterStatsFactory(CharacterRegistry characterRegistry) {
|
||||
this.characterRegistry = characterRegistry;
|
||||
}
|
||||
|
||||
public EntityStats Create(EntityAttributes attributes) {
|
||||
if(attributes == null) {
|
||||
throw new ArgumentNullException(nameof(attributes));
|
||||
if(attributes.attributes.All(a => a.value != 0)) {
|
||||
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
|
||||
}
|
||||
|
||||
//TODO: Create stats based on attributes
|
||||
|
||||
// int maxHealth = options.baseHealth + attributes.might * options.mightHealthBonus;
|
||||
// int maxStamina = options.baseStamina +
|
||||
// attributes.might * options.mightStaminaBonus +
|
||||
|
||||
@@ -3,16 +3,16 @@ using System;
|
||||
namespace Nox.Game {
|
||||
public static class DefaultCharacterSystemsFactory {
|
||||
public static ICharacterSystems Create(
|
||||
CharacterBaseSettings characterBaseSettings,
|
||||
StarterCharacterSettings starterCharacterSettings,
|
||||
PerksRegistry perksRegistry,
|
||||
CharacterRegistry characterRegistry,
|
||||
ModifiersRegistry modifiersRegistry) {
|
||||
IPerkFactory perkFactory = new PerkFactory(perksRegistry);
|
||||
IModfiersFactory modifiersFactory = new ModifiersFactory(modifiersRegistry);
|
||||
ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(characterRegistry);
|
||||
ICharacterStatsFactory statsFactory = new CharacterStatsFactory();
|
||||
ICharacterStatsFactory statsFactory = new CharacterStatsFactory(characterRegistry);
|
||||
ICharacterFactory characterFactory = new CharacterFactory(attributesFactory, statsFactory, perkFactory);
|
||||
IPartyFactory partyFactory = new PartyFactory(characterBaseSettings);
|
||||
IPartyFactory partyFactory = new PartyFactory(starterCharacterSettings);
|
||||
|
||||
return new CharacterSystems(perkFactory, modifiersFactory, characterFactory, partyFactory);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Nox.Game {
|
||||
public List<PartyDefinitionSet> partyDefinitionSets;
|
||||
|
||||
[Header("Testing Party Definition Sets")]
|
||||
public CharacterBaseSettings characterBaseSettings;
|
||||
public StarterCharacterSettings starterCharacterSettings;
|
||||
|
||||
private void OnValidate() {
|
||||
if(String.IsNullOrEmpty(startingSetId)) {
|
||||
@@ -47,9 +47,9 @@ namespace Nox.Game {
|
||||
private void ApplyClassAndRacialBonuses(PartyDefinitionSet testingSet) {
|
||||
var partyDefinition = testingSet.partyDefinition;
|
||||
foreach(var member in partyDefinition.members) {
|
||||
var baseSettings = characterBaseSettings.defaultEntityAttributes;
|
||||
var classAttributes = characterBaseSettings.classBonuses.FirstOrDefault(c => c.@class == member.@class)?.bonusAttributes;
|
||||
var racialAttributes = characterBaseSettings.racialBonuses.FirstOrDefault(rb => rb.race == member.race)?.bonusAttributes;
|
||||
var baseSettings = starterCharacterSettings.defaultEntityAttributes;
|
||||
var classAttributes = starterCharacterSettings.classBonuses.FirstOrDefault(c => c.@class == member.@class)?.bonusAttributes;
|
||||
var racialAttributes = starterCharacterSettings.racialBonuses.FirstOrDefault(rb => rb.race == member.race)?.bonusAttributes;
|
||||
if (classAttributes != null && racialAttributes != null) {
|
||||
member.Attributes += baseSettings + classAttributes + racialAttributes;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,22 @@ namespace Nox.Game {
|
||||
EntityStats Stats { get; }
|
||||
}
|
||||
|
||||
public enum StatType {
|
||||
None,
|
||||
Health,
|
||||
Stamina,
|
||||
Level,
|
||||
Experience
|
||||
}
|
||||
|
||||
public enum AttributeType {
|
||||
None,
|
||||
Might,
|
||||
Reflex,
|
||||
Knowledge,
|
||||
Perception
|
||||
}
|
||||
|
||||
public enum CharacterRole {
|
||||
None,
|
||||
Protagonist,
|
||||
@@ -33,29 +49,57 @@ namespace Nox.Game {
|
||||
Tunneler
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class Stat {
|
||||
private readonly StatType health;
|
||||
public StatType type;
|
||||
public int value;
|
||||
public Stat(StatType health, int value) {
|
||||
this.health = health;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Attribute {
|
||||
private readonly int values;
|
||||
public AttributeType attribute;
|
||||
public int value;
|
||||
public Attribute(AttributeType attributeType, int values) {
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class EntityAttributes {
|
||||
public int might;
|
||||
public int reflex;
|
||||
public int knowledge;
|
||||
public int perception;
|
||||
public Attribute[] attributes = {
|
||||
new(AttributeType.Might, 1),
|
||||
new(AttributeType.Reflex, 1),
|
||||
new(AttributeType.Knowledge, 1),
|
||||
new(AttributeType.Perception, 1)
|
||||
};
|
||||
|
||||
public static EntityAttributes operator +(EntityAttributes a, EntityAttributes b) {
|
||||
return new EntityAttributes {
|
||||
might = a.might + b.might,
|
||||
reflex = a.reflex + b.reflex,
|
||||
knowledge = a.knowledge + b.knowledge,
|
||||
perception = a.perception + b.perception
|
||||
attributes = a.attributes
|
||||
.Select(attr => new Attribute(attr.attribute, attr.value + b.attributes
|
||||
.First(attr2 => attr2.attribute == attr.attribute).value))
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class EntityStats {
|
||||
public int health;
|
||||
public int stamina;
|
||||
public int level;
|
||||
public int experience;
|
||||
public Stat[] stats = {
|
||||
new(StatType.Health, 0),
|
||||
new(StatType.Stamina, 0),
|
||||
new(StatType.Level, 1),
|
||||
new(StatType.Experience, 0)
|
||||
};
|
||||
|
||||
public int GetValue(StatType statType) {
|
||||
return stats.First(stat => stat.type == statType).value;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -65,8 +109,8 @@ namespace Nox.Game {
|
||||
public CharacterRace race;
|
||||
public CharacterClass @class;
|
||||
public CharacterRole role;
|
||||
[SerializeField] EntityAttributes attributes;
|
||||
[SerializeField] EntityStats stats;
|
||||
[SerializeField] private EntityAttributes attributes;
|
||||
[SerializeField] private EntityStats stats;
|
||||
public PerksData perksData = new();
|
||||
public ModifiersData activeModifiers = new();
|
||||
|
||||
@@ -77,15 +121,8 @@ namespace Nox.Game {
|
||||
role = role,
|
||||
race = race,
|
||||
@class = @class,
|
||||
attributes = new EntityAttributes {
|
||||
might = attributes?.might ?? 1,
|
||||
reflex = attributes?.reflex ?? 1,
|
||||
knowledge = attributes?.knowledge ?? 1
|
||||
},
|
||||
stats = new EntityStats {
|
||||
health = stats?.health ?? 1,
|
||||
stamina = stats?.stamina ?? 1
|
||||
},
|
||||
attributes = new EntityAttributes(),
|
||||
stats = new EntityStats(),
|
||||
perksData = new PerksData(),
|
||||
activeModifiers = new ModifiersData()
|
||||
};
|
||||
@@ -100,6 +137,7 @@ namespace Nox.Game {
|
||||
get => displayName;
|
||||
set => displayName = value;
|
||||
}
|
||||
|
||||
public EntityAttributes Attributes {
|
||||
get => attributes;
|
||||
set => attributes = value;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Nox.Game {
|
||||
bool TryAddModifier(CharacterDefinition character, string modiferId);
|
||||
}
|
||||
|
||||
public enum ModifierType {
|
||||
public enum ModifierRole {
|
||||
None,
|
||||
Flat,
|
||||
Addition,
|
||||
@@ -20,8 +20,11 @@ namespace Nox.Game {
|
||||
|
||||
[Serializable]
|
||||
public sealed class ModifierDefinition {
|
||||
public ModifierIds id;
|
||||
public ModifierType type;
|
||||
[ReadOnlyField]
|
||||
public System.Guid id = Guid.NewGuid();
|
||||
public StatType statType;
|
||||
public AttributeType attributeType;
|
||||
public ModifierRole role;
|
||||
public float value;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Nox.Game {
|
||||
public enum ModifierIds {
|
||||
generic_mgt_health_multiplier,
|
||||
warrior_mgt_health_multiplier,
|
||||
generic_mgt_mana_multiplier,
|
||||
generic_kno_mana_multiplier
|
||||
}
|
||||
|
||||
public enum PerksIds{
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc2631d096794cea9ae7c9cd82a38c53
|
||||
timeCreated: 1774366047
|
||||
@@ -19,46 +19,46 @@ namespace Nox.Game {
|
||||
});
|
||||
|
||||
CharacterTemplate[] companionTemplates = {
|
||||
new() {
|
||||
id = "companion-bruiser",
|
||||
displayName = "Rook",
|
||||
attributes = new EntityAttributes { might = 5, reflex = 2, knowledge = 1 },
|
||||
perksData = new PerksData(){
|
||||
perks = new List<PerkDefinition>()
|
||||
}
|
||||
},
|
||||
new() {
|
||||
id = "companion-scout",
|
||||
displayName = "Sable",
|
||||
attributes = new EntityAttributes { might = 2, reflex = 5, knowledge = 1 },
|
||||
perksData = new PerksData(){
|
||||
perks = new List<PerkDefinition>()
|
||||
}
|
||||
},
|
||||
new() {
|
||||
id = "companion-scholar",
|
||||
displayName = "Quill",
|
||||
attributes = new EntityAttributes { might = 1, reflex = 2, knowledge = 5 },
|
||||
perksData = new PerksData(){
|
||||
perks = new List<PerkDefinition>()
|
||||
}
|
||||
},
|
||||
new() {
|
||||
id = "companion-vanguard",
|
||||
displayName = "Brant",
|
||||
attributes = new EntityAttributes { might = 4, reflex = 3, knowledge = 2 },
|
||||
perksData = new PerksData(){
|
||||
perks = new List<PerkDefinition>()
|
||||
}
|
||||
},
|
||||
new() {
|
||||
id = "companion-tracker",
|
||||
displayName = "Mira",
|
||||
attributes = new EntityAttributes { might = 2, reflex = 4, knowledge = 3 },
|
||||
perksData = new PerksData(){
|
||||
perks = new List<PerkDefinition>()
|
||||
}
|
||||
}
|
||||
// new() {
|
||||
// id = "companion-bruiser",
|
||||
// displayName = "Rook",
|
||||
// attributes = new EntityAttributes { might = 5, reflex = 2, knowledge = 1 },
|
||||
// perksData = new PerksData(){
|
||||
// perks = new List<PerkDefinition>()
|
||||
// }
|
||||
// },
|
||||
// new() {
|
||||
// id = "companion-scout",
|
||||
// displayName = "Sable",
|
||||
// attributes = new EntityAttributes { might = 2, reflex = 5, knowledge = 1 },
|
||||
// perksData = new PerksData(){
|
||||
// perks = new List<PerkDefinition>()
|
||||
// }
|
||||
// },
|
||||
// new() {
|
||||
// id = "companion-scholar",
|
||||
// displayName = "Quill",
|
||||
// attributes = new EntityAttributes { might = 1, reflex = 2, knowledge = 5 },
|
||||
// perksData = new PerksData(){
|
||||
// perks = new List<PerkDefinition>()
|
||||
// }
|
||||
// },
|
||||
// new() {
|
||||
// id = "companion-vanguard",
|
||||
// displayName = "Brant",
|
||||
// attributes = new EntityAttributes { might = 4, reflex = 3, knowledge = 2 },
|
||||
// perksData = new PerksData(){
|
||||
// perks = new List<PerkDefinition>()
|
||||
// }
|
||||
// },
|
||||
// new() {
|
||||
// id = "companion-tracker",
|
||||
// displayName = "Mira",
|
||||
// attributes = new EntityAttributes { might = 2, reflex = 4, knowledge = 3 },
|
||||
// perksData = new PerksData(){
|
||||
// perks = new List<PerkDefinition>()
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
var companions = new List<CharacterDefinition>();
|
||||
|
||||
@@ -8,10 +8,10 @@ namespace Nox.Game {
|
||||
}
|
||||
|
||||
public sealed class PartyFactory : IPartyFactory {
|
||||
private readonly CharacterBaseSettings characterBaseSettings;
|
||||
private readonly StarterCharacterSettings starterCharacterSettings;
|
||||
|
||||
public PartyFactory(CharacterBaseSettings characterBaseSettings) {
|
||||
this.characterBaseSettings = characterBaseSettings;
|
||||
public PartyFactory(StarterCharacterSettings starterCharacterSettings) {
|
||||
this.starterCharacterSettings = starterCharacterSettings;
|
||||
}
|
||||
|
||||
public PartyDefinition Create(CharacterDefinition protagonist, IEnumerable<CharacterDefinition> companions = null) {
|
||||
@@ -20,7 +20,7 @@ namespace Nox.Game {
|
||||
}
|
||||
|
||||
var party = new PartyDefinition {
|
||||
maxPartySize = characterBaseSettings.maxPartySize <= 0 ? int.MaxValue : characterBaseSettings.maxPartySize
|
||||
maxPartySize = starterCharacterSettings.maxPartySize <= 0 ? int.MaxValue : starterCharacterSettings.maxPartySize
|
||||
};
|
||||
|
||||
var protagonistClone = protagonist.Clone();
|
||||
|
||||
@@ -3,7 +3,7 @@ using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "CharacterBaseSettings", menuName = "Nox/Database/Entities/CharacterBaseSettings")]
|
||||
public class CharacterBaseSettings: ScriptableObject {
|
||||
public class StarterCharacterSettings: ScriptableObject {
|
||||
[Header("Character Creation Defaults")]
|
||||
public DistributionPointsPerClass[] distributionPointsPerClass;
|
||||
public EntityAttributes defaultEntityAttributes;
|
||||
@@ -21,16 +21,16 @@ MonoBehaviour:
|
||||
points: 10
|
||||
- class: 4
|
||||
points: 10
|
||||
defaultEntityAttributes:
|
||||
might: 1
|
||||
reflex: 1
|
||||
knowledge: 1
|
||||
perception: 1
|
||||
defaultEntityStats:
|
||||
health: 0
|
||||
stamina: 0
|
||||
level: 0
|
||||
experience: 0
|
||||
stats:
|
||||
- type: 0
|
||||
value: 0
|
||||
- type: 1
|
||||
value: 0
|
||||
- type: 2
|
||||
value: 1
|
||||
- type: 3
|
||||
value: 0
|
||||
defaultPerksData:
|
||||
perks: []
|
||||
defaultModifiersData:
|
||||
@@ -38,9 +38,6 @@ MonoBehaviour:
|
||||
- id: 0
|
||||
type: 3
|
||||
value: 3
|
||||
- id: 1
|
||||
type: 3
|
||||
value: 4
|
||||
- id: 2
|
||||
type: 3
|
||||
value: 1
|
||||
@@ -49,64 +46,67 @@ MonoBehaviour:
|
||||
value: 2
|
||||
racialBonuses:
|
||||
- race: 1
|
||||
bonusAttributes:
|
||||
might: 0
|
||||
reflex: 0
|
||||
knowledge: 1
|
||||
perception: 0
|
||||
bonusStats:
|
||||
health: 0
|
||||
stamina: 0
|
||||
level: 0
|
||||
experience: 0
|
||||
stats:
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 1
|
||||
- type: 0
|
||||
value: 0
|
||||
startingPerks:
|
||||
perks: []
|
||||
permanentModifiers:
|
||||
modifiers: []
|
||||
- race: 2
|
||||
bonusAttributes:
|
||||
might: 0
|
||||
reflex: 1
|
||||
knowledge: 0
|
||||
perception: 0
|
||||
bonusStats:
|
||||
health: 0
|
||||
stamina: 0
|
||||
level: 0
|
||||
experience: 0
|
||||
stats:
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 1
|
||||
- type: 0
|
||||
value: 0
|
||||
startingPerks:
|
||||
perks: []
|
||||
permanentModifiers:
|
||||
modifiers: []
|
||||
- race: 2
|
||||
bonusAttributes:
|
||||
might: 1
|
||||
reflex: 0
|
||||
knowledge: 0
|
||||
perception: 0
|
||||
bonusStats:
|
||||
health: 0
|
||||
stamina: 0
|
||||
level: 0
|
||||
experience: 0
|
||||
stats:
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 1
|
||||
- type: 0
|
||||
value: 0
|
||||
startingPerks:
|
||||
perks: []
|
||||
permanentModifiers:
|
||||
modifiers: []
|
||||
classBonuses:
|
||||
- class: 1
|
||||
bonusAttributes:
|
||||
might: 0
|
||||
reflex: 0
|
||||
knowledge: 0
|
||||
perception: 0
|
||||
bonusStats:
|
||||
health: 0
|
||||
stamina: 0
|
||||
level: 0
|
||||
experience: 0
|
||||
stats:
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 10
|
||||
- type: 0
|
||||
value: 1
|
||||
- type: 0
|
||||
value: 0
|
||||
startingPerks:
|
||||
perks: []
|
||||
permanentModifiers:
|
||||
modifiers: []
|
||||
modifiers:
|
||||
- id: 1
|
||||
type: 3
|
||||
value: 4
|
||||
maxPartySize: 4
|
||||
|
||||
8
Packages/com.jovian.logger/Editor.meta
Normal file
8
Packages/com.jovian.logger/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61591a804f18a0c4684be901a22095d9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2224
Packages/com.jovian.logger/Editor/CustomConsole.cs
Normal file
2224
Packages/com.jovian.logger/Editor/CustomConsole.cs
Normal file
File diff suppressed because it is too large
Load Diff
2
Packages/com.jovian.logger/Editor/CustomConsole.cs.meta
Normal file
2
Packages/com.jovian.logger/Editor/CustomConsole.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd8ce1f3fd630a147b25c4578865941e
|
||||
18
Packages/com.jovian.logger/Editor/Jovian.LoggerEditor.asmdef
Normal file
18
Packages/com.jovian.logger/Editor/Jovian.LoggerEditor.asmdef
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Jovian.LoggerEditor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:9e11523c9d4d45445a0938098559d830"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 206337e0b2cdd1b448d7d752a7ca77e8
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
67
Packages/com.jovian.logger/Editor/JovianProjectSettings.cs
Normal file
67
Packages/com.jovian.logger/Editor/JovianProjectSettings.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.ProjectSettings {
|
||||
/// <summary>
|
||||
/// Reads and writes settings to ProjectSettings/CustomLoggerSettings.json.
|
||||
/// Flat key-value store. Use prefixed keys by convention (e.g. "logger.myKey").
|
||||
/// </summary>
|
||||
internal static class JovianProjectSettings {
|
||||
private const string FileName = "CustomLoggerSettings.json";
|
||||
|
||||
private static string FilePath {
|
||||
get {
|
||||
var fullName = Directory.GetParent(Application.dataPath)?.FullName;
|
||||
return Path.Combine(fullName ?? Application.dataPath, "ProjectSettings", FileName);
|
||||
}
|
||||
}
|
||||
|
||||
internal static T Get<T>(string packagePrefix, string setting, T defaultValue) {
|
||||
var root = LoadRoot();
|
||||
var key = $"{packagePrefix}.{setting}";
|
||||
if (root.TryGetValue(key, out var token)) {
|
||||
try {
|
||||
return token.ToObject<T>();
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
internal static void Set<T>(string packagePrefix, string setting, T value) {
|
||||
var root = LoadRoot();
|
||||
var key = $"{packagePrefix}.{setting}";
|
||||
root[key] = value != null ? JToken.FromObject(value) : JValue.CreateNull();
|
||||
SaveRoot(root);
|
||||
}
|
||||
|
||||
private static JObject LoadRoot() {
|
||||
string path = FilePath;
|
||||
if (!File.Exists(path)) {
|
||||
return new JObject();
|
||||
}
|
||||
|
||||
try {
|
||||
string json = File.ReadAllText(path);
|
||||
return JObject.Parse(json);
|
||||
} catch {
|
||||
return new JObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveRoot(JObject root) {
|
||||
string path = FilePath;
|
||||
string dir = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) {
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
string json = root.ToString(Formatting.Indented);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eff2fe1736b6efc40b7e52153c3b1010
|
||||
277
Packages/com.jovian.logger/Editor/LoggerSettingsEditor.cs
Normal file
277
Packages/com.jovian.logger/Editor/LoggerSettingsEditor.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
[CustomEditor(typeof(LoggerSettings))]
|
||||
internal class LoggerSettingsEditor : Editor {
|
||||
private static SerializedObject LoggerSettingsObj;
|
||||
|
||||
private SerializedProperty enableGlobalLoggingProp;
|
||||
private SerializedProperty uploadBaseUrlProp;
|
||||
private SerializedProperty minimumTimeBetweenUploadsProp;
|
||||
private SerializedProperty loggerColorsProp;
|
||||
|
||||
|
||||
// For loggerColors sub-fields
|
||||
private SerializedProperty infoColorProp;
|
||||
private SerializedProperty warningColorProp;
|
||||
private SerializedProperty errorColorProp;
|
||||
private SerializedProperty assertColorProp;
|
||||
private SerializedProperty exceptionColorProp;
|
||||
private SerializedProperty spamColorProp;
|
||||
|
||||
private static Editor editor;
|
||||
private bool showLocalFilters = true;
|
||||
private bool showAllFilters = true;
|
||||
private bool showGlobalFilters = true;
|
||||
private string globalCallerNames = "";
|
||||
private string localCallerNames = "";
|
||||
private bool hasGlobalSavedFilter = true;
|
||||
private bool hasLocalSavedFilter = true;
|
||||
|
||||
private class ListOfFilters {
|
||||
public List<Filters> filters;
|
||||
}
|
||||
|
||||
private void LoadLocalFilters() {
|
||||
var loggerSettings = (LoggerSettings)target;
|
||||
var filters = EditorPrefs.GetString("LoggerFilters");
|
||||
if(UnityIsHeadless()) {
|
||||
filters = null;
|
||||
}
|
||||
if(!string.IsNullOrEmpty(filters)) {
|
||||
loggerSettings.LocalFilters = JsonConvert.DeserializeObject<ListOfFilters>(filters).filters;
|
||||
}
|
||||
|
||||
if(loggerSettings.LocalFilters == null) {
|
||||
loggerSettings.LocalFilters = new List<Filters>(0);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UnityIsHeadless() {
|
||||
if(Environment.CommandLine.Contains("-batchmode")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnEnable() {
|
||||
enableGlobalLoggingProp = serializedObject.FindProperty("enableGlobalLogging");
|
||||
uploadBaseUrlProp = serializedObject.FindProperty("uploadBaseUrl");
|
||||
minimumTimeBetweenUploadsProp = serializedObject.FindProperty("minimumTimeBetweenUploads");
|
||||
loggerColorsProp = serializedObject.FindProperty("loggerColors");
|
||||
|
||||
infoColorProp = loggerColorsProp.FindPropertyRelative("infoColor");
|
||||
warningColorProp = loggerColorsProp.FindPropertyRelative("warningColor");
|
||||
errorColorProp = loggerColorsProp.FindPropertyRelative("errorColor");
|
||||
assertColorProp = loggerColorsProp.FindPropertyRelative("assertColor");
|
||||
exceptionColorProp = loggerColorsProp.FindPropertyRelative("exceptionColor");
|
||||
spamColorProp = loggerColorsProp.FindPropertyRelative("spamColor");
|
||||
|
||||
LoadLocalFilters();
|
||||
}
|
||||
|
||||
public static void ShowSettings() {
|
||||
var loggerSettings = new AssetSettingsLoader<LoggerSettings>().GetSettings(LoggerSettingsProvider.SETTINGS_FILE);
|
||||
LoggerSettingsObj = new SerializedObject(loggerSettings);
|
||||
LoggerSettingsObj.Update();
|
||||
editor ??= CreateEditor(loggerSettings);
|
||||
editor.OnInspectorGUI();
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI() {
|
||||
serializedObject.Update();
|
||||
var loggerSettings = (LoggerSettings)target;
|
||||
|
||||
EditorGUILayout.Space(15);
|
||||
EditorGUILayout.LabelField("Custom Logger Settings", EditorStyles.whiteLargeLabel);
|
||||
EditorGUILayout.Space(15);
|
||||
EditorGUILayout.HelpBox("This will enable/disable logging for the entire project. It has a code level method as well.", MessageType.Warning);
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.PropertyField(enableGlobalLoggingProp, new GUIContent("Enable Global Logging"));
|
||||
|
||||
EditorGUILayout.Space(15);
|
||||
EditorGUILayout.LabelField("Log Message Colors", EditorStyles.whiteLargeLabel);
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.PropertyField(infoColorProp, new GUIContent("Info Color"));
|
||||
EditorGUILayout.PropertyField(warningColorProp, new GUIContent("Warning Color"));
|
||||
EditorGUILayout.PropertyField(errorColorProp, new GUIContent("Error Color"));
|
||||
EditorGUILayout.PropertyField(assertColorProp, new GUIContent("Assert Color"));
|
||||
EditorGUILayout.PropertyField(exceptionColorProp, new GUIContent("Exception Color"));
|
||||
EditorGUILayout.PropertyField(spamColorProp, new GUIContent("Spam Color"));
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
if(GUILayout.Button("Reset To Default Colors")) {
|
||||
loggerSettings.ResetColorsToDefault();
|
||||
EditorUtility.SetDirty(loggerSettings);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(20);
|
||||
EditorGUILayout.LabelField("Log Uploader Settings", EditorStyles.whiteLargeLabel);
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.PropertyField(uploadBaseUrlProp, new GUIContent("Upload Base URL"));
|
||||
EditorGUILayout.PropertyField(minimumTimeBetweenUploadsProp, new GUIContent("Min Time Between Uploads (sec)"));
|
||||
|
||||
EditorGUILayout.Space(30);
|
||||
showAllFilters = EditorGUILayout.Foldout(showAllFilters, "Logger Filters");
|
||||
if(showAllFilters) {
|
||||
EditorGUILayout.Space(20);
|
||||
EditorGUILayout.LabelField("Global Project Level Filters", EditorStyles.whiteLargeLabel);
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.HelpBox("Global filters will be saved with the asset and, if commited, applied to everyone using the project", MessageType.Warning);
|
||||
EditorGUILayout.Space(3);
|
||||
showGlobalFilters = EditorGUILayout.Foldout(showGlobalFilters, "Active Filters");
|
||||
if(showGlobalFilters) {
|
||||
for(int i = 0; i < loggerSettings.globalFilters.Length; i++) {
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
loggerSettings.globalFilters[i].logCategory = (LogCategory)EditorGUILayout.EnumFlagsField("Log Category", loggerSettings.globalFilters[i].logCategory);
|
||||
loggerSettings.globalFilters[i].jovianLogType = (JovianLogType)EditorGUILayout.EnumPopup("Log Type", loggerSettings.globalFilters[i].jovianLogType);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
globalCallerNames = GetCallersAsString(loggerSettings.globalFilters[i].callerNames);
|
||||
globalCallerNames = EditorGUILayout.TextField("Caller Name", globalCallerNames);
|
||||
loggerSettings.globalFilters[i].callerListingType = (CallerListingType)EditorGUILayout.EnumPopup("", loggerSettings.globalFilters[i].callerListingType, GUILayout.Width(120));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
loggerSettings.globalFilters[i].callerNames = globalCallerNames.Split(",").ToList();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if(GUILayout.Button("Remove This Filter")) {
|
||||
var temp = loggerSettings.globalFilters.ToList();
|
||||
temp.RemoveAt(i);
|
||||
loggerSettings.globalFilters = temp.ToArray();
|
||||
EditorUtility.SetDirty(loggerSettings);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.RequestScriptReload();
|
||||
}
|
||||
|
||||
if(GUILayout.Button("Save Changes")) {
|
||||
hasGlobalSavedFilter = true;
|
||||
EditorUtility.SetDirty(loggerSettings);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.RequestScriptReload();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (!hasGlobalSavedFilter) {
|
||||
GUILayout.Space(10);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.HelpBox("Filter Not Saved...", MessageType.Error);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
GUILayout.Space(5);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if(GUILayout.Button("Add Global Filter")) {
|
||||
var temp = loggerSettings.globalFilters.ToList();
|
||||
temp.Add(new Filters());
|
||||
loggerSettings.globalFilters = temp.ToArray();
|
||||
hasGlobalSavedFilter = false;
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(20);
|
||||
EditorGUILayout.LabelField("Local Editor Level Filters (Not Serialized)", EditorStyles.whiteLargeLabel);
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.HelpBox("Local filters are local and will not be saved with the asset but rather with the editor settings. Use these to set personal filters.", MessageType.Info);
|
||||
EditorGUILayout.Space(3);
|
||||
showLocalFilters = EditorGUILayout.Foldout(showLocalFilters, "Active Filters");
|
||||
if(showLocalFilters) {
|
||||
for(int i = 0; i < loggerSettings.LocalFilters.Count; i++) {
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
EditorGUI.BeginChangeCheck();
|
||||
loggerSettings.LocalFilters[i].logCategory = (LogCategory)EditorGUILayout.EnumFlagsField("Log Category", loggerSettings.LocalFilters[i].logCategory);
|
||||
loggerSettings.LocalFilters[i].jovianLogType = (JovianLogType)EditorGUILayout.EnumPopup("Log Type", loggerSettings.LocalFilters[i].jovianLogType);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
localCallerNames = GetCallersAsString(loggerSettings.LocalFilters[i].callerNames);
|
||||
localCallerNames = EditorGUILayout.TextField("Caller Name", localCallerNames, GUILayout.MinWidth(400));
|
||||
loggerSettings.LocalFilters[i].callerListingType = (CallerListingType)EditorGUILayout.EnumPopup("", loggerSettings.LocalFilters[i].callerListingType, GUILayout.Width(120));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
loggerSettings.LocalFilters[i].callerNames = localCallerNames.Split(",").ToList();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if(GUILayout.Button("Remove This Filter")) {
|
||||
var temp = new List<Filters>(loggerSettings.LocalFilters);
|
||||
temp.RemoveAt(i);
|
||||
loggerSettings.LocalFilters = temp;
|
||||
var filters = JsonConvert.SerializeObject(new ListOfFilters() { filters = loggerSettings.LocalFilters });
|
||||
EditorPrefs.SetString("LoggerFilters", filters);
|
||||
EditorUtility.RequestScriptReload();
|
||||
}
|
||||
|
||||
if(GUILayout.Button("Save Changes")) {
|
||||
var filters = JsonConvert.SerializeObject(new ListOfFilters() { filters = loggerSettings.LocalFilters });
|
||||
EditorPrefs.SetString("LoggerFilters", filters);
|
||||
EditorUtility.RequestScriptReload();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (!hasLocalSavedFilter) {
|
||||
GUILayout.Space(10);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.HelpBox("Filter Not Saved...", MessageType.Error);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
GUILayout.Space(5);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if(GUILayout.Button("Add Local Filter")) {
|
||||
loggerSettings.LocalFilters.Add(new Filters());
|
||||
hasLocalSavedFilter = false;
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private string GetCallersAsString(List<string> callerNames) {
|
||||
if(callerNames == null || callerNames.Count == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
string result = "";
|
||||
for(int i = 0; i < callerNames.Count; i++) {
|
||||
result += callerNames[i];
|
||||
if(i != callerNames.Count - 1) {
|
||||
result += ",";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnDisable() {
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e97c6cb58b8e0f4f990c51f841842af
|
||||
60
Packages/com.jovian.logger/Editor/LoggerSettingsProvider.cs
Normal file
60
Packages/com.jovian.logger/Editor/LoggerSettingsProvider.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
internal sealed class LoggerSettingsProvider : SettingsProvider {
|
||||
public LoggerSettingsProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null) : base(path, scopes, keywords) { }
|
||||
|
||||
public const string SETTINGS_FILE = "Assets/Settings/Resources/logger-settings.asset";
|
||||
|
||||
[SettingsProvider]
|
||||
public static SettingsProvider CreateSettingsProvider() {
|
||||
var provider = new SettingsProvider("Project/Jovian/Logger", SettingsScope.Project) {
|
||||
guiHandler = (searchContext) => { LoggerSettingsEditor.ShowSettings(); }
|
||||
};
|
||||
return provider;
|
||||
}
|
||||
|
||||
private class ConfigLoader : AssetPostprocessor {
|
||||
private static AssetSettingsLoader<LoggerSettings> loader;
|
||||
|
||||
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
|
||||
loader ??= new AssetSettingsLoader<LoggerSettings>();
|
||||
loader.GetSettings(SETTINGS_FILE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AssetSettingsLoader<T> where T : ScriptableObject {
|
||||
private T settings;
|
||||
|
||||
/// <summary>
|
||||
/// Get the settings file if cached/existent. If it doesn't exist, it will create one.
|
||||
/// </summary>
|
||||
/// <param name="settingsFilePath"></param>
|
||||
/// <returns></returns>
|
||||
public T GetSettings(string settingsFilePath) {
|
||||
return settings ?? SetSettings(settingsFilePath);
|
||||
}
|
||||
|
||||
private T SetSettings(string settingsFilePath) {
|
||||
if(!Directory.Exists(Path.GetDirectoryName(settingsFilePath))) {
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(settingsFilePath));
|
||||
}
|
||||
|
||||
settings = AssetDatabase.LoadAssetAtPath<T>(settingsFilePath);
|
||||
if(settings != null) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
settings = ScriptableObject.CreateInstance<T>();
|
||||
AssetDatabase.CreateAsset(settings, settingsFilePath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
settings = AssetDatabase.LoadAssetAtPath<T>(settingsFilePath);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 360fda9a575ab7b4eaf523256ac39663
|
||||
245
Packages/com.jovian.logger/Editor/RemoteLogReceiver.cs
Normal file
245
Packages/com.jovian.logger/Editor/RemoteLogReceiver.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
/// <summary>
|
||||
/// TCP server that receives log messages from RemoteLogSender on device builds.
|
||||
/// Managed by Custom Console — not intended for standalone use.
|
||||
/// </summary>
|
||||
internal static class RemoteLogReceiver {
|
||||
public const int DefaultPort = 9876;
|
||||
|
||||
public struct RemoteLogEntry {
|
||||
public string message;
|
||||
public string stackTrace;
|
||||
public string timestamp;
|
||||
public int frameCount;
|
||||
public JovianLogType type;
|
||||
public LogCategory logCategory;
|
||||
public bool isCustomLog;
|
||||
}
|
||||
|
||||
public struct RemoteWatchEntry {
|
||||
public string key;
|
||||
public string value;
|
||||
public LogCategory logCategory;
|
||||
public string timestamp;
|
||||
public int frameCount;
|
||||
}
|
||||
|
||||
public static event Action<RemoteLogEntry> OnRemoteLog;
|
||||
public static event Action<RemoteWatchEntry> OnRemoteWatch;
|
||||
public static event Action<string> OnRemoteUnwatch;
|
||||
public static event Action<string> OnClientConnected;
|
||||
public static event Action OnClientDisconnected;
|
||||
|
||||
private static TcpListener listener;
|
||||
private static Thread listenThread;
|
||||
private static volatile bool running;
|
||||
private static int port = DefaultPort;
|
||||
|
||||
public static bool IsRunning => running;
|
||||
public static int Port => port;
|
||||
public static string ConnectedClientName { get; private set; } = "";
|
||||
public static bool HasClient { get; private set; }
|
||||
|
||||
private static readonly ConcurrentQueue<Action> mainThreadQueue = new ConcurrentQueue<Action>();
|
||||
|
||||
public static void Start(int listenPort = DefaultPort) {
|
||||
if (running) return;
|
||||
port = listenPort;
|
||||
|
||||
try {
|
||||
listener = new TcpListener(IPAddress.Any, port);
|
||||
listener.Start();
|
||||
running = true;
|
||||
|
||||
listenThread = new Thread(ListenLoop) {
|
||||
IsBackground = true,
|
||||
Name = "RemoteLogReceiver"
|
||||
};
|
||||
listenThread.Start();
|
||||
|
||||
EditorApplication.update += ProcessMainThreadQueue;
|
||||
Debug.Log($"[RemoteLog] Listening on port {port}");
|
||||
} catch (Exception e) {
|
||||
Debug.LogError($"[RemoteLog] Failed to start listener: {e.Message}");
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Stop() {
|
||||
running = false;
|
||||
HasClient = false;
|
||||
ConnectedClientName = "";
|
||||
|
||||
try { listener?.Stop(); } catch { }
|
||||
listener = null;
|
||||
|
||||
EditorApplication.update -= ProcessMainThreadQueue;
|
||||
}
|
||||
|
||||
private static void ProcessMainThreadQueue() {
|
||||
while (mainThreadQueue.TryDequeue(out var action)) {
|
||||
action?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ListenLoop() {
|
||||
while (running) {
|
||||
try {
|
||||
if (!listener.Pending()) {
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
var client = listener.AcceptTcpClient();
|
||||
client.NoDelay = true;
|
||||
client.ReceiveTimeout = 0;
|
||||
HandleClient(client);
|
||||
} catch (SocketException) {
|
||||
if (!running) break;
|
||||
} catch (ObjectDisposedException) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleClient(TcpClient client) {
|
||||
var thread = new Thread(() => ClientReadLoop(client)) {
|
||||
IsBackground = true,
|
||||
Name = "RemoteLogClient"
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private static void ClientReadLoop(TcpClient client) {
|
||||
try {
|
||||
using var stream = client.GetStream();
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
|
||||
HasClient = true;
|
||||
|
||||
while (running && client.Connected) {
|
||||
string line = reader.ReadLine();
|
||||
if (line == null) break;
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
ParseAndDispatch(line);
|
||||
}
|
||||
} catch (IOException) {
|
||||
// Connection lost
|
||||
} catch (ObjectDisposedException) {
|
||||
// Shutting down
|
||||
} finally {
|
||||
try { client.Close(); } catch { }
|
||||
HasClient = false;
|
||||
ConnectedClientName = "";
|
||||
mainThreadQueue.Enqueue(() => OnClientDisconnected?.Invoke());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseAndDispatch(string json) {
|
||||
try {
|
||||
// Check for handshake
|
||||
if (json.Contains("\"handshake\"")) {
|
||||
string appName = ExtractJsonString(json, "app");
|
||||
string platform = ExtractJsonString(json, "platform");
|
||||
ConnectedClientName = $"{appName} ({platform})";
|
||||
mainThreadQueue.Enqueue(() => OnClientConnected?.Invoke(ConnectedClientName));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for watch
|
||||
if (json.Contains("\"watch\"")) {
|
||||
var watchEntry = new RemoteWatchEntry {
|
||||
key = ExtractJsonString(json, "wk"),
|
||||
value = ExtractJsonString(json, "wv"),
|
||||
logCategory = (LogCategory)ExtractJsonInt(json, "c"),
|
||||
timestamp = ExtractJsonString(json, "ts"),
|
||||
frameCount = ExtractJsonInt(json, "fc"),
|
||||
};
|
||||
mainThreadQueue.Enqueue(() => OnRemoteWatch?.Invoke(watchEntry));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for unwatch
|
||||
if (json.Contains("\"unwatch\"")) {
|
||||
string key = ExtractJsonString(json, "wk");
|
||||
mainThreadQueue.Enqueue(() => OnRemoteUnwatch?.Invoke(key));
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new RemoteLogEntry {
|
||||
message = ExtractJsonString(json, "m"),
|
||||
stackTrace = ExtractJsonString(json, "s"),
|
||||
timestamp = ExtractJsonString(json, "ts"),
|
||||
frameCount = ExtractJsonInt(json, "fc"),
|
||||
type = (JovianLogType)ExtractJsonInt(json, "t"),
|
||||
logCategory = (LogCategory)ExtractJsonInt(json, "c"),
|
||||
isCustomLog = ExtractJsonBool(json, "f"),
|
||||
};
|
||||
|
||||
mainThreadQueue.Enqueue(() => OnRemoteLog?.Invoke(entry));
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"[RemoteLog] Failed to parse: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Lightweight JSON field extractors (avoids dependency on full JSON parser for simple protocol)
|
||||
private static string ExtractJsonString(string json, string key) {
|
||||
string pattern = "\"" + key + "\":\"";
|
||||
int start = json.IndexOf(pattern, StringComparison.Ordinal);
|
||||
if (start < 0) return "";
|
||||
start += pattern.Length;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = start; i < json.Length; i++) {
|
||||
char c = json[i];
|
||||
if (c == '\\' && i + 1 < json.Length) {
|
||||
char next = json[i + 1];
|
||||
switch (next) {
|
||||
case '"': sb.Append('"'); i++; break;
|
||||
case '\\': sb.Append('\\'); i++; break;
|
||||
case 'n': sb.Append('\n'); i++; break;
|
||||
case 'r': sb.Append('\r'); i++; break;
|
||||
case 't': sb.Append('\t'); i++; break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
} else if (c == '"') {
|
||||
break;
|
||||
} else {
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static int ExtractJsonInt(string json, string key) {
|
||||
string pattern = "\"" + key + "\":";
|
||||
int start = json.IndexOf(pattern, StringComparison.Ordinal);
|
||||
if (start < 0) return 0;
|
||||
start += pattern.Length;
|
||||
|
||||
int end = start;
|
||||
while (end < json.Length && (char.IsDigit(json[end]) || json[end] == '-')) end++;
|
||||
if (end == start) return 0;
|
||||
return int.TryParse(json.AsSpan(start, end - start), out int val) ? val : 0;
|
||||
}
|
||||
|
||||
private static bool ExtractJsonBool(string json, string key) {
|
||||
string pattern = "\"" + key + "\":";
|
||||
int start = json.IndexOf(pattern, StringComparison.Ordinal);
|
||||
if (start < 0) return false;
|
||||
start += pattern.Length;
|
||||
return start < json.Length && json[start] == 't';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13353efce044337428c052b7e37f0445
|
||||
8
Packages/com.jovian.logger/Runtime.meta
Normal file
8
Packages/com.jovian.logger/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68a0fd172189d17458408cca0bc439a9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
181
Packages/com.jovian.logger/Runtime/GlobalLogger.cs
Normal file
181
Packages/com.jovian.logger/Runtime/GlobalLogger.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
/// <summary>
|
||||
/// Default logger for Custom Logger in case you don't want to create a new instance of CustomLogger
|
||||
/// Recommended is to create a new instance of CustomLogger
|
||||
/// </summary>
|
||||
public static class GlobalLogger {
|
||||
|
||||
/// <summary>
|
||||
/// Logs messages that are meant to only be seen in the editor
|
||||
/// </summary>
|
||||
/// <param name="msg">Log message</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogSpam(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogSpam($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs messages that are meant to only be seen in the editor with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg">Log message</param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogSpam(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogSpam($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Info/Debug messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogInfo(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogInfo($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Info/Debug messagesc with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="logCat"></param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogInfo(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogInfo($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Warning messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogWarning(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogWarning($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Warning messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogWarning(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogWarning($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Error messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogError(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogError($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Error messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogError(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogError($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Assert messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogAssert(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogAssert($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Assert messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogAssert(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogAssert($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Logs string Exception messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogException(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs string Exception messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogException(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Exception messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogException(Exception e, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", e.Message, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Exception messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
/// <param name="context">Object to select in scene/project</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LogException(Exception e, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
|
||||
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", e.Message, logCat, context);
|
||||
}
|
||||
|
||||
private static Type GetCaller() {
|
||||
var stackTrace = new StackTrace();
|
||||
var stackFrame = stackTrace.GetFrame(2);
|
||||
var caller = stackFrame?.GetMethod()?.DeclaringType;
|
||||
return caller;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.logger/Runtime/GlobalLogger.cs.meta
Normal file
2
Packages/com.jovian.logger/Runtime/GlobalLogger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 351a08a96ffb9654b94e50b31c1058ba
|
||||
193
Packages/com.jovian.logger/Runtime/InternalLogger.cs
Normal file
193
Packages/com.jovian.logger/Runtime/InternalLogger.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
internal class LoggerSettingsData {
|
||||
public bool enableGlobalLogging = true;
|
||||
public Filters[] globalFilters = Array.Empty<Filters>();
|
||||
public LoggerColors loggerColors = new();
|
||||
public bool isLoaded = false;
|
||||
#if UNITY_EDITOR
|
||||
public List<Filters> LocalFilters { get; set; } = new();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static class InternalLogger {
|
||||
private static readonly LoggerSettingsData loggerSettingsData = new();
|
||||
private static bool enableGlobalLogging = true;
|
||||
internal static bool IsMainThread => LoggerUtility.IsMainThread;
|
||||
|
||||
internal static void LoadSettings() {
|
||||
var loggerSettings = LoggerUtility.LoadCustomLoggerSettings();
|
||||
|
||||
if(!loggerSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
var colors = loggerSettings.loggerColors;
|
||||
loggerSettingsData.globalFilters = loggerSettings.globalFilters;
|
||||
enableGlobalLogging = loggerSettings.enableGlobalLogging;
|
||||
loggerSettingsData.loggerColors.infoColor = colors.infoColor;
|
||||
loggerSettingsData.loggerColors.warningColor = colors.warningColor;
|
||||
loggerSettingsData.loggerColors.errorColor = colors.errorColor;
|
||||
loggerSettingsData.loggerColors.assertColor = colors.assertColor;
|
||||
loggerSettingsData.loggerColors.exceptionColor = colors.exceptionColor;
|
||||
loggerSettingsData.loggerColors.spamColor = colors.spamColor;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if(!Environment.CommandLine.Contains("-batchmode")) {
|
||||
loggerSettingsData.LocalFilters = loggerSettings.LocalFilters;
|
||||
} else {
|
||||
loggerSettings.LocalFilters.Clear();
|
||||
}
|
||||
|
||||
Debug.Log("[CustomLogger] Local Filters: " + loggerSettingsData.LocalFilters.Count);
|
||||
#endif
|
||||
loggerSettingsData.isLoaded = true;
|
||||
Debug.Log("[CustomLogger] Global Filters: " + loggerSettingsData.globalFilters.Length);
|
||||
Debug.Log("[CustomLogger] Settings loaded");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void LogInternal(JovianLogType jovianLogType, LogCategory logcat, string classType, string msg, Color color, string args = "", UnityEngine.Object reference = null) {
|
||||
if(!enableGlobalLogging) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!loggerSettingsData.isLoaded && IsMainThread) {
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
// Check if any filters are present and apply filters
|
||||
foreach(var filter in loggerSettingsData.globalFilters) {
|
||||
if(filter == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if((filter.jovianLogType < jovianLogType || !filter.logCategory.HasFlag(logcat)) || !FilterCaller(filter.callerNames, filter.callerListingType)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if(!Environment.CommandLine.Contains("-batchmode")) {
|
||||
foreach(var filter in loggerSettingsData.LocalFilters) {
|
||||
if(filter == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if((filter.jovianLogType < jovianLogType || !filter.logCategory.HasFlag(logcat)) || !FilterCaller(filter.callerNames, filter.callerListingType)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(jovianLogType == JovianLogType.Spam) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
StringBuilder sb = new(500);
|
||||
switch(jovianLogType) {
|
||||
case JovianLogType.Spam:
|
||||
sb.Append("SPAM -> ");
|
||||
break;
|
||||
case JovianLogType.Info:
|
||||
sb.Append("INFO -> ");
|
||||
break;
|
||||
case JovianLogType.Warning:
|
||||
sb.Append("WARNING -> ");
|
||||
break;
|
||||
case JovianLogType.Error:
|
||||
sb.Append("ERROR -> ");
|
||||
break;
|
||||
case JovianLogType.Assert:
|
||||
sb.Append("ASSERT -> ");
|
||||
break;
|
||||
case JovianLogType.Exception:
|
||||
sb.Append("EXCEPTION -> ");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
var isFrameCountEnabled = LoggerUtility.IsFrameCountEnabled;
|
||||
|
||||
// As exceptions can be reported to Unity Exception tracking, we do not want frame numbers in the message. It will prevent Unity from grouping reports
|
||||
var logTypeShouldIncludeFrameCount = jovianLogType != JovianLogType.Exception;
|
||||
if(isFrameCountEnabled && logTypeShouldIncludeFrameCount) {
|
||||
sb.Append("F:");
|
||||
sb.Append(LoggerUtility.FrameCount);
|
||||
sb.Append(" |");
|
||||
}
|
||||
|
||||
sb.Append(" [");
|
||||
sb.Append(logcat);
|
||||
sb.Append("] ");
|
||||
sb.Append("[");
|
||||
sb.Append(classType);
|
||||
sb.Append("] ");
|
||||
sb.Append(msg);
|
||||
var message = sb.ToString();
|
||||
LoggerUtility.FormattedLogCallback?.Invoke((jovianLogType, logcat, message));
|
||||
|
||||
#if UNITY_EDITOR
|
||||
//remove the color when not in the editor to avoid cluttering the log files
|
||||
if(!Environment.CommandLine.Contains("-batchmode") && IsMainThread) {
|
||||
message = $"<color=#{ColorUtility.ToHtmlStringRGB(color)}>{message}</color>";
|
||||
}
|
||||
#endif
|
||||
Debug.unityLogger.Log(LoggerUtility.GetLogType(jovianLogType), (object)message, reference);
|
||||
return;
|
||||
|
||||
bool FilterCaller(List<string> filterCallerNames, CallerListingType filterCallerListingType) {
|
||||
foreach(var caller in filterCallerNames) {
|
||||
if(!string.IsNullOrEmpty(caller)) {
|
||||
switch(filterCallerListingType) {
|
||||
case CallerListingType.Blacklist_Caller:
|
||||
return !classType.Contains(caller);
|
||||
case CallerListingType.Whitelist_Caller:
|
||||
return classType.Contains(caller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void LogSpam(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
|
||||
LogInternal(JovianLogType.Spam, logcat, caller, msg, loggerSettingsData.loggerColors.spamColor, "spam", reference);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void LogInfo(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
|
||||
LogInternal(JovianLogType.Info, logcat, caller, msg, loggerSettingsData.loggerColors.infoColor, "", reference);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void LogWarning(string classType, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
|
||||
LogInternal(JovianLogType.Warning, logcat, classType, msg, loggerSettingsData.loggerColors.warningColor, "", reference);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void LogError(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
|
||||
LogInternal(JovianLogType.Error, logcat, caller, msg, loggerSettingsData.loggerColors.errorColor, "", reference);
|
||||
}
|
||||
|
||||
internal static void LogException(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
|
||||
LogInternal(JovianLogType.Exception, logcat, caller, msg, loggerSettingsData.loggerColors.exceptionColor, "", reference);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void LogAssert(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
|
||||
LogInternal(JovianLogType.Assert, logcat, caller, msg, loggerSettingsData.loggerColors.assertColor, "", reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1463245ed08381f4688ab74cc4296ba1
|
||||
14
Packages/com.jovian.logger/Runtime/Jovian.Logger.asmdef
Normal file
14
Packages/com.jovian.logger/Runtime/Jovian.Logger.asmdef
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Jovian.Logger",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e11523c9d4d45445a0938098559d830
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
36
Packages/com.jovian.logger/Runtime/LogCategory.cs
Normal file
36
Packages/com.jovian.logger/Runtime/LogCategory.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
[Flags]
|
||||
public enum LogCategory {
|
||||
None = 0,
|
||||
General = 1,
|
||||
Editor = 2,
|
||||
Core = 4,
|
||||
GameLogic = 8,
|
||||
UI = 16,
|
||||
Input = 32,
|
||||
Network = 64,
|
||||
Analytics = 128,
|
||||
Audio = 256,
|
||||
Graphics = 512,
|
||||
Physics = 1024,
|
||||
AI = 2048,
|
||||
Internal = 4096,
|
||||
Testing = 8192
|
||||
}
|
||||
|
||||
public enum CallerListingType {
|
||||
Blacklist_Caller,
|
||||
Whitelist_Caller
|
||||
}
|
||||
|
||||
public enum JovianLogType {
|
||||
Exception,
|
||||
Assert,
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
Spam
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.logger/Runtime/LogCategory.cs.meta
Normal file
2
Packages/com.jovian.logger/Runtime/LogCategory.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edd66ef15598e84448ef22155f418e66
|
||||
280
Packages/com.jovian.logger/Runtime/Logger.cs
Normal file
280
Packages/com.jovian.logger/Runtime/Logger.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
/// <summary>
|
||||
/// The recommended way to log messages in Jovian Logger.
|
||||
/// Create a new instance of this struct in the class you want to log messages.
|
||||
/// </summary>
|
||||
public struct Logger {
|
||||
private string caller;
|
||||
private LogCategory logCategory;
|
||||
|
||||
public Logger(Type caller, LogCategory logCategory) : this() {
|
||||
this.logCategory = logCategory;
|
||||
this.caller = caller.Name;
|
||||
LoggerUtility.PreloadLoggerSettings();
|
||||
}
|
||||
|
||||
private string Caller {
|
||||
get {
|
||||
if(string.IsNullOrEmpty(caller)) {
|
||||
caller = "Unspecified";
|
||||
}
|
||||
return caller;
|
||||
}
|
||||
}
|
||||
|
||||
private LogCategory LogCategory {
|
||||
get {
|
||||
if(logCategory == LogCategory.None) {
|
||||
logCategory = LogCategory.General;
|
||||
}
|
||||
return logCategory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs messages that are meant to only be seen in the editor
|
||||
/// </summary>
|
||||
/// <param name="msg">Log message</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogSpam(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogSpam($"{Caller}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs messages that are meant to only be seen in the editor with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg">Log message</param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogSpam(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogSpam($"{Caller}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Info/Debug messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogInfo(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogInfo($"{Caller}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Info/Debug messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogInfo(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogInfo($"{Caller}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Warning messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogWarning(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogWarning($"{Caller}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Warning messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogWarning(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogWarning($"{Caller}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Error messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogError(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogError($"{Caller}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Error messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogError(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogError($"{Caller}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Assert messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogAssert(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogAssert($"{Caller}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Assert messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogAssert(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogAssert($"{Caller}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs string Exception messages
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogException(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogException($"{Caller}.{callerMethod}", msg, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs string Exception messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogException(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogException($"{Caller}.{callerMethod}", msg, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Exception messages
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogException(Exception e, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogException($"{Caller}.{callerMethod}", e.Message, logCat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs standard Exception messages with a context object
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
/// <param name="context">Object to focus on in scene/project window</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
/// <param name="callerMethod">Implicit, do not provide</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void LogException(Exception e, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
InternalLogger.LogException($"{Caller}.{callerMethod}", e.Message, logCat, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a watch variable that updates in place in the Custom Console instead of creating new entries.
|
||||
/// Ideal for values that change every frame (positions, FPS, state names, etc.).
|
||||
/// </summary>
|
||||
/// <param name="key">Unique identifier for this watch (e.g. "playerPos", "fps")</param>
|
||||
/// <param name="value">The current value to display</param>
|
||||
/// <param name="logCat">Optional Log Category</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Watch(string key, object value, LogCategory logCat = LogCategory.None) {
|
||||
if(logCat == LogCategory.None) {
|
||||
logCat = LogCategory;
|
||||
}
|
||||
|
||||
LoggerUtility.WatchCallback?.Invoke((key, value?.ToString() ?? "null", logCat));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a watch variable from the Custom Console.
|
||||
/// </summary>
|
||||
/// <param name="key">The watch key to remove</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Unwatch(string key) {
|
||||
LoggerUtility.UnwatchCallback?.Invoke(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.logger/Runtime/Logger.cs.meta
Normal file
2
Packages/com.jovian.logger/Runtime/Logger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb1cfb4712ed8a649a051a11d0aa17f5
|
||||
36
Packages/com.jovian.logger/Runtime/LoggerSettings.cs
Normal file
36
Packages/com.jovian.logger/Runtime/LoggerSettings.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
public class LoggerSettings : ScriptableObject {
|
||||
public bool enableGlobalLogging = true;
|
||||
public Filters[] globalFilters = Array.Empty<Filters>();
|
||||
public LoggerColors loggerColors = new();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public List<Filters> LocalFilters { get; set; } = new();
|
||||
#endif
|
||||
public void ResetColorsToDefault() {
|
||||
loggerColors = new LoggerColors();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LoggerColors {
|
||||
public Color infoColor = Color.white;
|
||||
public Color warningColor = Color.yellow;
|
||||
public Color errorColor = Color.red;
|
||||
public Color assertColor = new(1f, 0.3f, 0.2f);
|
||||
public Color exceptionColor = new(1f, 0.0f, 0.7f);
|
||||
public Color spamColor = Color.grey;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Filters {
|
||||
public LogCategory logCategory = (LogCategory)~0;
|
||||
public JovianLogType jovianLogType = JovianLogType.Spam;
|
||||
public List<string> callerNames = new();
|
||||
public CallerListingType callerListingType = CallerListingType.Blacklist_Caller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67fe3f48aa2b3b349a7b99381bebb12d
|
||||
152
Packages/com.jovian.logger/Runtime/LoggerUtility.cs
Normal file
152
Packages/com.jovian.logger/Runtime/LoggerUtility.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
public static class LoggerUtility {
|
||||
private static LoggerSettings loggerSettings;
|
||||
private static bool setByFrameCount = true;
|
||||
private static int frameCount;
|
||||
private static bool isLoaded;
|
||||
private static readonly int mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current code is executing on the main Unity thread.
|
||||
/// </summary>
|
||||
public static bool IsMainThread => Thread.CurrentThread.ManagedThreadId == mainThreadId;
|
||||
|
||||
/// <summary>
|
||||
/// Callback to post formatted log messages.
|
||||
/// </summary>
|
||||
public static Action<(JovianLogType, LogCategory, string)> FormattedLogCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback for watch-mode logs that update in place instead of creating new entries.
|
||||
/// Parameters: (watchKey, value, logCategory)
|
||||
/// </summary>
|
||||
public static Action<(string key, string value, LogCategory category)> WatchCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback when a watch key is removed.
|
||||
/// </summary>
|
||||
public static Action<string> UnwatchCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If enabled it will show either the Unity's Time.frameCount or the custom frame count set by the user. This is a global setting.
|
||||
/// </summary>
|
||||
public static int FrameCount {
|
||||
get {
|
||||
if(setByFrameCount) {
|
||||
try {
|
||||
frameCount = Time.frameCount;
|
||||
} catch(UnityException) {
|
||||
// Time.frameCount can only be called from the main thread.
|
||||
// Return last known value when called from a background thread.
|
||||
}
|
||||
}
|
||||
|
||||
return frameCount;
|
||||
}
|
||||
set {
|
||||
frameCount = value;
|
||||
setByFrameCount = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static LoggerSettings LoadCustomLoggerSettings() {
|
||||
try {
|
||||
if(isLoaded) {
|
||||
return loggerSettings;
|
||||
}
|
||||
|
||||
loggerSettings = Resources.Load<LoggerSettings>("logger-settings");
|
||||
isLoaded = true;
|
||||
return loggerSettings;
|
||||
} catch {
|
||||
//Debug.Log($"[Exception] LoggerSettings could not be loaded.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the frame counting feature to be included with the logs on/off
|
||||
/// </summary>
|
||||
/// <param name="enable"></param>
|
||||
public static void ToggleFrameCount(bool enable) {
|
||||
IsFrameCountEnabled = enable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable/Disable logging globally
|
||||
/// </summary>
|
||||
/// <param name="enable"></param>
|
||||
public static void ToggleLogging(bool enable) {
|
||||
loggerSettings.enableGlobalLogging = enable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the frame count feature is enabled
|
||||
/// </summary>
|
||||
public static bool IsFrameCountEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Load settings preemptively to avoid asynchronous loading for unity assets.
|
||||
/// </summary>
|
||||
public static void PreloadLoggerSettings() {
|
||||
try {
|
||||
LoadCustomLoggerSettings();
|
||||
} catch(Exception e) {
|
||||
Debug.Log($"[Exception] LoggerSettings could not be loaded. {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom filter programmatically. Useful for setting up remote filters
|
||||
/// </summary>
|
||||
/// <param name="logCategory">Required set of log categories</param>
|
||||
/// <param name="jovianLogLevel">The log level, default being level 4, Exception(in unity it is the base log level)</param>
|
||||
/// <param name="callerNames">A list of classes that should be watched, default null</param>
|
||||
/// <param name="clearAll">If all preexisting filters should be first removed</param>
|
||||
public static void AddFilter(LogCategory logCategory, JovianLogType jovianLogLevel = JovianLogType.Spam, List<string> callerNames = null, bool clearAll = false) {
|
||||
var filter = new Filters() {
|
||||
jovianLogType = jovianLogLevel,
|
||||
logCategory = logCategory,
|
||||
callerNames = callerNames
|
||||
};
|
||||
var loggerSettings = LoadCustomLoggerSettings();
|
||||
if(clearAll) {
|
||||
loggerSettings.globalFilters = new Filters[1];
|
||||
loggerSettings.globalFilters[0] = filter;
|
||||
InternalLogger.LoadSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
var filters = new Filters[loggerSettings.globalFilters.Length + 1];
|
||||
for(var i = 0; i < loggerSettings.globalFilters.Length; i++) {
|
||||
filters[i] = loggerSettings.globalFilters[i];
|
||||
}
|
||||
|
||||
filters[^1] = filter;
|
||||
loggerSettings.globalFilters = filters;
|
||||
InternalLogger.LoadSettings();
|
||||
}
|
||||
|
||||
public static void ReloadLoggerSettings() {
|
||||
InternalLogger.LoadSettings();
|
||||
}
|
||||
|
||||
public static UnityEngine.LogType GetLogType(JovianLogType jovianLogType) {
|
||||
return jovianLogType switch {
|
||||
JovianLogType.Info => UnityEngine.LogType.Log,
|
||||
JovianLogType.Warning => UnityEngine.LogType.Warning,
|
||||
JovianLogType.Error => UnityEngine.LogType.Error,
|
||||
JovianLogType.Assert => UnityEngine.LogType.Assert,
|
||||
JovianLogType.Exception => UnityEngine.LogType.Exception,
|
||||
JovianLogType.Spam => UnityEngine.LogType.Log,
|
||||
_ => UnityEngine.LogType.Log
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.logger/Runtime/LoggerUtility.cs.meta
Normal file
2
Packages/com.jovian.logger/Runtime/LoggerUtility.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89f12f254c1d11e418ec326092b0b049
|
||||
270
Packages/com.jovian.logger/Runtime/RemoteLogSender.cs
Normal file
270
Packages/com.jovian.logger/Runtime/RemoteLogSender.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
/// <summary>
|
||||
/// Sends log messages to the Custom Console editor window over TCP.
|
||||
/// Add this component to a GameObject in your scene (or create one at runtime)
|
||||
/// to enable remote logging from device builds.
|
||||
/// </summary>
|
||||
public class RemoteLogSender : MonoBehaviour {
|
||||
[Tooltip("IP address of the machine running the Unity Editor")]
|
||||
[SerializeField] private string editorHost = "127.0.0.1";
|
||||
[Tooltip("Port the Custom Console is listening on")]
|
||||
[SerializeField] private int editorPort = 9876;
|
||||
[Tooltip("Automatically connect on start")]
|
||||
[SerializeField] private bool autoConnect = true;
|
||||
[Tooltip("Retry connection every N seconds when disconnected")]
|
||||
[SerializeField] private float reconnectInterval = 5f;
|
||||
|
||||
private TcpClient client;
|
||||
private NetworkStream stream;
|
||||
private readonly ConcurrentQueue<string> sendQueue = new ConcurrentQueue<string>();
|
||||
private Thread sendThread;
|
||||
private volatile bool running;
|
||||
private volatile bool connected;
|
||||
private float reconnectTimer;
|
||||
|
||||
private static RemoteLogSender instance;
|
||||
|
||||
/// <summary>Whether the sender is currently connected to the editor.</summary>
|
||||
public bool IsConnected => connected;
|
||||
|
||||
/// <summary>The editor host address.</summary>
|
||||
public string EditorHost {
|
||||
get => editorHost;
|
||||
set => editorHost = value;
|
||||
}
|
||||
|
||||
/// <summary>The editor port.</summary>
|
||||
public int EditorPort {
|
||||
get => editorPort;
|
||||
set => editorPort = value;
|
||||
}
|
||||
|
||||
private void Awake() {
|
||||
if (instance != null && instance != this) {
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void OnEnable() {
|
||||
Application.logMessageReceivedThreaded += OnUnityLogReceived;
|
||||
LoggerUtility.FormattedLogCallback += OnJovianLogReceived;
|
||||
LoggerUtility.WatchCallback += OnWatch;
|
||||
LoggerUtility.UnwatchCallback += OnUnwatch;
|
||||
|
||||
if (autoConnect) {
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable() {
|
||||
Application.logMessageReceivedThreaded -= OnUnityLogReceived;
|
||||
LoggerUtility.FormattedLogCallback -= OnJovianLogReceived;
|
||||
LoggerUtility.WatchCallback -= OnWatch;
|
||||
LoggerUtility.UnwatchCallback -= OnUnwatch;
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void Update() {
|
||||
if (!connected && autoConnect) {
|
||||
reconnectTimer += Time.unscaledDeltaTime;
|
||||
if (reconnectTimer >= reconnectInterval) {
|
||||
reconnectTimer = 0f;
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Connect to the Custom Console editor.</summary>
|
||||
public void Connect() {
|
||||
if (connected) return;
|
||||
|
||||
try {
|
||||
client = new TcpClient();
|
||||
client.NoDelay = true;
|
||||
client.SendTimeout = 2000;
|
||||
var result = client.BeginConnect(editorHost, editorPort, null, null);
|
||||
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(2));
|
||||
if (!success || !client.Connected) {
|
||||
client.Close();
|
||||
client = null;
|
||||
return;
|
||||
}
|
||||
client.EndConnect(result);
|
||||
stream = client.GetStream();
|
||||
connected = true;
|
||||
running = true;
|
||||
|
||||
sendThread = new Thread(SendLoop) {
|
||||
IsBackground = true,
|
||||
Name = "RemoteLogSender"
|
||||
};
|
||||
sendThread.Start();
|
||||
|
||||
// Send handshake
|
||||
EnqueueMessage("{\"handshake\":true,\"app\":\"" + Application.productName + "\",\"platform\":\"" + Application.platform + "\"}");
|
||||
Debug.Log($"[RemoteLog] Connected to editor at {editorHost}:{editorPort}");
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"[RemoteLog] Failed to connect: {e.Message}");
|
||||
CleanupConnection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disconnect from the editor.</summary>
|
||||
public void Disconnect() {
|
||||
running = false;
|
||||
CleanupConnection();
|
||||
}
|
||||
|
||||
private void CleanupConnection() {
|
||||
connected = false;
|
||||
try { stream?.Close(); } catch { }
|
||||
try { client?.Close(); } catch { }
|
||||
stream = null;
|
||||
client = null;
|
||||
}
|
||||
|
||||
private void OnJovianLogReceived((JovianLogType logType, LogCategory logCategory, string message) log) {
|
||||
if (!connected) return;
|
||||
var json = BuildLogJson(log.message, "", (int)log.logType, (int)log.logCategory, true,
|
||||
DateTime.Now.ToString("HH:mm:ss.fff"), Time.frameCount);
|
||||
EnqueueMessage(json);
|
||||
}
|
||||
|
||||
private static readonly string[] LoggerPrefixes = {
|
||||
"INFO -> ", "ERROR -> ", "WARNING -> ",
|
||||
"EXCEPTION -> ", "ASSERT -> ", "SPAM -> "
|
||||
};
|
||||
|
||||
private static bool IsLoggerFormattedMessage(string condition) {
|
||||
foreach (var prefix in LoggerPrefixes) {
|
||||
if (condition.StartsWith(prefix, StringComparison.Ordinal)) return true;
|
||||
}
|
||||
if (condition.StartsWith("<color=#", StringComparison.Ordinal) && condition.Length > 22) {
|
||||
var afterTag = condition.AsSpan(15);
|
||||
foreach (var prefix in LoggerPrefixes) {
|
||||
if (afterTag.StartsWith(prefix.AsSpan(), StringComparison.Ordinal)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnUnityLogReceived(string condition, string stackTrace, UnityEngine.LogType type) {
|
||||
if (!connected) return;
|
||||
// Skip logger-formatted messages — those come via OnCustomLog
|
||||
if (IsLoggerFormattedMessage(condition)) return;
|
||||
|
||||
int loggerTyper = type switch {
|
||||
LogType.Error => (int)JovianLogType.Error,
|
||||
LogType.Assert => (int)JovianLogType.Assert,
|
||||
LogType.Warning => (int)JovianLogType.Warning,
|
||||
LogType.Exception => (int)JovianLogType.Exception,
|
||||
_ => (int)JovianLogType.Info,
|
||||
};
|
||||
|
||||
string message = condition;
|
||||
if (type is LogType.Error or LogType.Assert or LogType.Exception && !string.IsNullOrEmpty(stackTrace)) {
|
||||
message = $"{condition}\n{stackTrace}";
|
||||
}
|
||||
|
||||
var json = BuildLogJson(message, stackTrace ?? "", loggerTyper, (int)LogCategory.General, false,
|
||||
DateTime.Now.ToString("HH:mm:ss.fff"), Time.frameCount);
|
||||
EnqueueMessage(json);
|
||||
}
|
||||
|
||||
private void OnWatch((string key, string value, LogCategory category) watch) {
|
||||
if (!connected) return;
|
||||
var sb = new StringBuilder(128);
|
||||
sb.Append("{\"watch\":true,\"wk\":");
|
||||
AppendJsonString(sb, watch.key);
|
||||
sb.Append(",\"wv\":");
|
||||
AppendJsonString(sb, watch.value);
|
||||
sb.Append(",\"c\":").Append((int)watch.category);
|
||||
sb.Append(",\"ts\":");
|
||||
AppendJsonString(sb, DateTime.Now.ToString("HH:mm:ss.fff"));
|
||||
sb.Append(",\"fc\":").Append(Time.frameCount);
|
||||
sb.Append('}');
|
||||
EnqueueMessage(sb.ToString());
|
||||
}
|
||||
|
||||
private void OnUnwatch(string key) {
|
||||
if (!connected) return;
|
||||
var sb = new StringBuilder(64);
|
||||
sb.Append("{\"unwatch\":true,\"wk\":");
|
||||
AppendJsonString(sb, key);
|
||||
sb.Append('}');
|
||||
EnqueueMessage(sb.ToString());
|
||||
}
|
||||
|
||||
private static string BuildLogJson(string message, string stackTrace, int type, int category, bool isCustom, string timestamp, int frame) {
|
||||
// Manual JSON building to avoid allocations from JsonUtility
|
||||
var sb = new StringBuilder(256);
|
||||
sb.Append("{\"m\":");
|
||||
AppendJsonString(sb, message);
|
||||
sb.Append(",\"s\":");
|
||||
AppendJsonString(sb, stackTrace);
|
||||
sb.Append(",\"t\":").Append(type);
|
||||
sb.Append(",\"c\":").Append(category);
|
||||
sb.Append(",\"f\":").Append(isCustom ? "true" : "false");
|
||||
sb.Append(",\"ts\":");
|
||||
AppendJsonString(sb, timestamp);
|
||||
sb.Append(",\"fc\":").Append(frame);
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendJsonString(StringBuilder sb, string value) {
|
||||
if (value == null) { sb.Append("\"\""); return; }
|
||||
sb.Append('"');
|
||||
foreach (char c in value) {
|
||||
switch (c) {
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
sb.Append('"');
|
||||
}
|
||||
|
||||
private void EnqueueMessage(string json) {
|
||||
// Cap queue size to prevent memory issues if sending is slow
|
||||
if (sendQueue.Count < 10000) {
|
||||
sendQueue.Enqueue(json);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendLoop() {
|
||||
while (running) {
|
||||
try {
|
||||
if (sendQueue.TryDequeue(out string json)) {
|
||||
byte[] data = Encoding.UTF8.GetBytes(json + "\n");
|
||||
stream.Write(data, 0, data.Length);
|
||||
} else {
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
} catch (Exception) {
|
||||
running = false;
|
||||
connected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
if (instance == this) instance = null;
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 690a449164ff86c49b1c0c9c3b4ef3b1
|
||||
9
Packages/com.jovian.logger/package.json
Normal file
9
Packages/com.jovian.logger/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "com.jovian.logger",
|
||||
"displayName": "Jovian Logger",
|
||||
"version": "1.0.0",
|
||||
"description": "A custom logger package based logger that I created some time in the past.",
|
||||
"dependencies": {
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1"
|
||||
}
|
||||
}
|
||||
7
Packages/com.jovian.logger/package.json.meta
Normal file
7
Packages/com.jovian.logger/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 701d375fdedcd4741aed611b3054cbc9
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.jovian.recentassets/Editor.meta
Normal file
8
Packages/com.jovian.recentassets/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67dd42ef1f8eb4c4d82ef17efd711ead
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Jovian.AssetsHistory",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 924d34ce263e4c44189ac3e93726603c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
387
Packages/com.jovian.recentassets/Editor/RecentAssetsMenu.cs
Normal file
387
Packages/com.jovian.recentassets/Editor/RecentAssetsMenu.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Jovian.Recents {
|
||||
public class RecentAssets : EditorWindow {
|
||||
|
||||
[System.Serializable]
|
||||
private class AssetSelection {
|
||||
public bool expanded;
|
||||
public string assetPath;
|
||||
|
||||
[System.NonSerialized]
|
||||
private Object actualAsset;
|
||||
|
||||
public Object Asset {
|
||||
get {
|
||||
if(actualAsset == null) {
|
||||
actualAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||||
}
|
||||
return actualAsset;
|
||||
}
|
||||
set => actualAsset = value;
|
||||
}
|
||||
|
||||
public List<SubAssetSelection> subAssets;
|
||||
public bool isPinned;
|
||||
public string FileName => Path.GetFileName(assetPath);
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct SubAssetSelection {
|
||||
// In the current structure of this tool, to reload this object is quite messy.
|
||||
// The SubAsset doesn't know if the current context, e.g. scene, is the one it was previously selected from.
|
||||
// There might be another asset in this other scene also called "Main Camera" etc
|
||||
[System.NonSerialized]
|
||||
public Object subAsset;
|
||||
|
||||
public string subAssetPath;
|
||||
}
|
||||
|
||||
private bool subscribeToSelectionEvents;
|
||||
|
||||
private bool SubscribeToSelectionEvents {
|
||||
set {
|
||||
if(value) {
|
||||
Selection.selectionChanged -= OnSelectionChanged;
|
||||
Selection.selectionChanged += OnSelectionChanged;
|
||||
}
|
||||
else {
|
||||
Selection.selectionChanged -= OnSelectionChanged;
|
||||
}
|
||||
subscribeToSelectionEvents = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool subscribeToPrefabOpenEvents;
|
||||
|
||||
private bool SubscribeToPrefabOpen {
|
||||
set {
|
||||
PrefabStage.prefabStageOpened -= OnPrefabStageOpened;
|
||||
PrefabStage.prefabStageOpened += OnPrefabStageOpened;
|
||||
PrefabStage.prefabStageClosing -= OnPrefabStageClosing;
|
||||
PrefabStage.prefabStageClosing += OnPrefabStageClosing;
|
||||
subscribeToPrefabOpenEvents = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool subscribeToSceneOpenEvents;
|
||||
|
||||
private bool SubscribeToSceneOpenEvents {
|
||||
set {
|
||||
if(value) {
|
||||
EditorSceneManager.activeSceneChangedInEditMode -= OnActiveSceneChangedInEditMode;
|
||||
EditorSceneManager.activeSceneChangedInEditMode += OnActiveSceneChangedInEditMode;
|
||||
}
|
||||
else {
|
||||
EditorSceneManager.activeSceneChangedInEditMode -= OnActiveSceneChangedInEditMode;
|
||||
}
|
||||
subscribeToSceneOpenEvents = value;
|
||||
}
|
||||
}
|
||||
|
||||
private string EditorPrefsSettingsSelectionKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Selections";
|
||||
private string EditorPrefsSettingsPrefabsKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Prefabs";
|
||||
private string EditorPrefsSettingsScenesKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Scenes";
|
||||
private string EditorPrefsSettingsCountKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_MaxHistory";
|
||||
private string EditorPrefsSettingsHistoryKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections";
|
||||
|
||||
private const string PinnedIcon = "IN LockButton on";
|
||||
private const string UnPinnedIcon = "IN LockButton";
|
||||
|
||||
[System.Serializable]
|
||||
private class JsonWrapper {
|
||||
public JsonWrapper(List<AssetSelection> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public List<AssetSelection> data;
|
||||
}
|
||||
|
||||
private List<AssetSelection> selectionHistory = new();
|
||||
private int maxHistory = 10;
|
||||
private Vector2 scrollPos;
|
||||
|
||||
private PrefabStage prefabStage;
|
||||
private GUIStyle guiStyle;
|
||||
|
||||
[MenuItem("Jovian/Assets History...", false, 20)]
|
||||
private static void Init() {
|
||||
var window = GetWindow<RecentAssets>(false, "Assets History");
|
||||
window.minSize = new Vector2(100f, 100f);
|
||||
window.Focus();
|
||||
}
|
||||
|
||||
private void OnEnable() {
|
||||
// In case of Editor reload, perhaps because of recompile, events needs to be resubscribed
|
||||
LoadSettings();
|
||||
SubscribeToSelectionEvents = subscribeToSelectionEvents;
|
||||
SubscribeToSceneOpenEvents = subscribeToSceneOpenEvents;
|
||||
SubscribeToPrefabOpen = subscribeToPrefabOpenEvents;
|
||||
|
||||
LoadEntries();
|
||||
}
|
||||
|
||||
private void OnDisable() {
|
||||
SaveEntries();
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
SubscribeToSelectionEvents = false;
|
||||
SubscribeToSceneOpenEvents = false;
|
||||
SubscribeToPrefabOpen = false;
|
||||
}
|
||||
private void SaveEntries() {
|
||||
var jsonList = JsonUtility.ToJson(new JsonWrapper(selectionHistory));
|
||||
EditorPrefs.SetString(EditorPrefsSettingsHistoryKey, jsonList);
|
||||
}
|
||||
|
||||
private void SaveSettings() {
|
||||
EditorPrefs.SetBool(EditorPrefsSettingsSelectionKey, subscribeToSelectionEvents);
|
||||
EditorPrefs.SetBool(EditorPrefsSettingsPrefabsKey, subscribeToPrefabOpenEvents);
|
||||
EditorPrefs.SetBool(EditorPrefsSettingsScenesKey, subscribeToSceneOpenEvents);
|
||||
EditorPrefs.SetInt(EditorPrefsSettingsCountKey, maxHistory);
|
||||
}
|
||||
|
||||
private void LoadEntries() {
|
||||
var jsonList = EditorPrefs.GetString(EditorPrefsSettingsHistoryKey);
|
||||
var oldList = JsonUtility.FromJson<JsonWrapper>(jsonList)?.data;
|
||||
if(oldList != null) {
|
||||
selectionHistory = oldList;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettings() {
|
||||
if(EditorPrefs.HasKey(EditorPrefsSettingsSelectionKey)) {
|
||||
subscribeToSelectionEvents = EditorPrefs.GetBool(EditorPrefsSettingsSelectionKey);
|
||||
}
|
||||
if(EditorPrefs.HasKey(EditorPrefsSettingsPrefabsKey)) {
|
||||
subscribeToPrefabOpenEvents = EditorPrefs.GetBool(EditorPrefsSettingsPrefabsKey);
|
||||
}
|
||||
if(EditorPrefs.HasKey(EditorPrefsSettingsScenesKey)) {
|
||||
subscribeToSceneOpenEvents = EditorPrefs.GetBool(EditorPrefsSettingsScenesKey);
|
||||
}
|
||||
if(EditorPrefs.HasKey(EditorPrefsSettingsCountKey)) {
|
||||
maxHistory = EditorPrefs.GetInt(EditorPrefsSettingsCountKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActiveSceneChangedInEditMode(Scene previousScene, Scene newScene) {
|
||||
var selectedObject = AssetDatabase.LoadMainAssetAtPath(newScene.path);
|
||||
if(selectedObject != null) {
|
||||
OnAssetInteracted(selectedObject, newScene.path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnPrefabStageOpened(PrefabStage prefab) {
|
||||
// We actually need to know if we're in prefabStage because of how asset parenting works during prefab editing, e.g. when _subscribeToSelectionEvents==true;
|
||||
prefabStage = prefab;
|
||||
if(subscribeToPrefabOpenEvents) {
|
||||
OnAssetInteracted(prefab.prefabContentsRoot, prefab.assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrefabStageClosing(PrefabStage prefab) {
|
||||
prefabStage = null;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged() {
|
||||
var selectedObject = Selection.activeObject;
|
||||
if(!selectedObject) {
|
||||
return; // When you select something in e.g. Project View that isn't an asset or object, perhaps the category header "Packages"
|
||||
}
|
||||
string assetPath;
|
||||
if(prefabStage != null && selectedObject is GameObject selectedGameObject && prefabStage.IsPartOfPrefabContents(selectedGameObject)) {
|
||||
assetPath = prefabStage.assetPath; // If we're in a PrefabStage and selecting a child GO, the assetPath is the actual prefab itself, not this potentially nested entity
|
||||
}
|
||||
else {
|
||||
assetPath = AssetDatabase.GetAssetOrScenePath(selectedObject);
|
||||
}
|
||||
|
||||
OnAssetInteracted(selectedObject, assetPath);
|
||||
}
|
||||
|
||||
private void OnAssetInteracted(Object selectedObject, string assetPath) {
|
||||
AssetSelection selection = null;
|
||||
for(var i = 0; i < selectionHistory.Count; i++) {
|
||||
if(selectionHistory[i].assetPath == assetPath) {
|
||||
selection = selectionHistory[i];
|
||||
selectionHistory.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(selection == null) {
|
||||
// This is a new asset selection
|
||||
selection = new AssetSelection {
|
||||
expanded = false,
|
||||
assetPath = assetPath,
|
||||
Asset = AssetDatabase.LoadMainAssetAtPath(assetPath),
|
||||
subAssets = new List<SubAssetSelection>()
|
||||
};
|
||||
}
|
||||
|
||||
selectionHistory.Insert(0, selection); // Selection is now first in list
|
||||
|
||||
var subAssetSelection = new SubAssetSelection {
|
||||
subAsset = selectedObject,
|
||||
subAssetPath = selectedObject.name
|
||||
};
|
||||
for(var i = 0; i < selection.subAssets.Count; i++) {
|
||||
if(selection.subAssets[i].subAsset == selectedObject ||
|
||||
// In case of unloading and reloading a scene, we try to "reuse" entries to the same object.
|
||||
// If several objects exist by the same name, they're already null and become a new entry "anyway"
|
||||
(selection.subAssets[i].subAsset == null && selection.subAssets[i].subAssetPath == subAssetSelection.subAssetPath)) {
|
||||
selection.subAssets.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
selection.subAssets.Insert(0, subAssetSelection);
|
||||
|
||||
// while instead of if, since you can change the number of tracked assets
|
||||
while(selectionHistory.Count > maxHistory) {
|
||||
selectionHistory.RemoveAt(selectionHistory.Count - 1);
|
||||
}
|
||||
UpdatePinned();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void UpdatePinned() {
|
||||
var numberOfAlreadyPinnedEntries = 0;
|
||||
for(var i = 0; i < selectionHistory.Count; i++) {
|
||||
var currSelection = selectionHistory[i];
|
||||
if(currSelection.isPinned) {
|
||||
if(i == numberOfAlreadyPinnedEntries) {
|
||||
numberOfAlreadyPinnedEntries++;
|
||||
continue;
|
||||
}
|
||||
selectionHistory.RemoveAt(i);
|
||||
selectionHistory.Insert(numberOfAlreadyPinnedEntries, currSelection);
|
||||
numberOfAlreadyPinnedEntries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
var originalIconSize = EditorGUIUtility.GetIconSize();
|
||||
EditorGUIUtility.SetIconSize(new Vector2(16f, 16f));
|
||||
if(guiStyle == null) {
|
||||
guiStyle = new GUIStyle(GUI.skin.button) {
|
||||
alignment = TextAnchor.MiddleLeft
|
||||
};
|
||||
}
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
TightLabel("History count", "The number of recent assets currently tracked");
|
||||
TightLabel(selectionHistory.Count.ToString());
|
||||
if(GUILayout.Button("Clear", GUILayout.ExpandWidth(false))) {
|
||||
selectionHistory.Clear();
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
TightLabel("What to track", "Select for what interactions you want this list to be updated");
|
||||
var onSelection = GUILayout.Toggle(subscribeToSelectionEvents, "On selection", GUILayout.ExpandWidth(false));
|
||||
if(onSelection != subscribeToSelectionEvents) {
|
||||
SubscribeToSelectionEvents = onSelection;
|
||||
}
|
||||
EditorGUI.BeginDisabledGroup(subscribeToSelectionEvents); // If we're listening to selection the other two doesn't matter
|
||||
subscribeToPrefabOpenEvents = GUILayout.Toggle(subscribeToPrefabOpenEvents, "Open prefabs", GUILayout.ExpandWidth(false));
|
||||
SubscribeToSceneOpenEvents = GUILayout.Toggle(subscribeToSceneOpenEvents, "Open scenes", GUILayout.ExpandWidth(false));
|
||||
EditorGUI.EndDisabledGroup();
|
||||
GUILayout.FlexibleSpace();
|
||||
TightLabel("Max history", "The number of recent assets to track");
|
||||
maxHistory = EditorGUILayout.DelayedIntField(maxHistory, GUILayout.Width(50f));
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||
var pinStatusChanged = false;
|
||||
for(var i = 0; i < selectionHistory.Count; i++) {
|
||||
var selection = selectionHistory[i];
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
// Pinning
|
||||
var pinIcon = selection.isPinned ? PinnedIcon : UnPinnedIcon;
|
||||
if(GUILayout.Button(EditorGUIUtility.IconContent(pinIcon), GUILayout.Width(20f), GUILayout.ExpandWidth(false))) {
|
||||
selection.isPinned = !selection.isPinned;
|
||||
pinStatusChanged = true;
|
||||
}
|
||||
|
||||
// Open
|
||||
if(GUILayout.Button(EditorGUIUtility.IconContent("d_editicon.sml"), GUILayout.Width(26f), GUILayout.ExpandWidth(false))) {
|
||||
if(Event.current.button == 1) {
|
||||
// On right click we only ping the object.
|
||||
EditorGUIUtility.PingObject(selection.Asset);
|
||||
}
|
||||
else {
|
||||
AssetDatabase.OpenAsset(selection.Asset);
|
||||
}
|
||||
}
|
||||
|
||||
// Object
|
||||
GUI.enabled = false;
|
||||
if(selection.Asset != null) {
|
||||
EditorGUILayout.ObjectField(selection.Asset, typeof(Object), false);
|
||||
}
|
||||
else {
|
||||
var labelNotFound = new GUIContent($"{selection.FileName} not found.",
|
||||
EditorGUIUtility.ObjectContent(selection.Asset, typeof(Object)).image, selection.assetPath);
|
||||
EditorGUILayout.LabelField(labelNotFound);
|
||||
}
|
||||
GUI.enabled = true;
|
||||
|
||||
// Sub entries
|
||||
GUILayout.Label(GUIContent.none, GUILayout.MinWidth(80f), GUILayout.MaxWidth(80f));
|
||||
var rect = GUILayoutUtility.GetLastRect();
|
||||
selection.expanded = EditorGUI.Foldout(rect, selection.expanded, "Sub entries", true);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
if(selection.expanded) {
|
||||
EditorGUILayout.BeginVertical();
|
||||
EditorGUI.indentLevel++;
|
||||
for(var y = 0; y < selection.subAssets.Count; y++) {
|
||||
var subAsset = selection.subAssets[y];
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.Space(10f, false); // As an indent
|
||||
var label = new GUIContent(subAsset.subAssetPath, EditorGUIUtility.ObjectContent(subAsset.subAsset, typeof(Object)).image);
|
||||
if(GUILayout.Button(label, guiStyle, GUILayout.MinWidth(50f), GUILayout.MaxWidth(200f), GUILayout.ExpandWidth(false))) {
|
||||
if(Event.current.button == 1) {
|
||||
// On right click we only ping the object.
|
||||
EditorGUIUtility.PingObject(subAsset.subAsset);
|
||||
}
|
||||
else {
|
||||
Selection.activeObject = subAsset.subAsset;
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
EditorGUIUtility.SetIconSize(originalIconSize);
|
||||
if(pinStatusChanged) {
|
||||
UpdatePinned();
|
||||
}
|
||||
}
|
||||
|
||||
public static void TightLabel(string labelStr) {
|
||||
var label = new GUIContent(labelStr);
|
||||
TightLabel(label);
|
||||
}
|
||||
|
||||
public static void TightLabel(string labelStr, string tooltip) {
|
||||
var label = new GUIContent(labelStr, tooltip);
|
||||
TightLabel(label);
|
||||
}
|
||||
|
||||
public static void TightLabel(GUIContent label) {
|
||||
//This is the important bit, we set the width to the calculated width of the content in the GUIStyle of the control
|
||||
EditorGUILayout.LabelField(label, GUILayout.Width(GUI.skin.label.CalcSize(label).x));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 537374a9d1704fb408713e76df4b85f9
|
||||
6
Packages/com.jovian.recentassets/package.json
Normal file
6
Packages/com.jovian.recentassets/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "com.jovian.assets-history",
|
||||
"displayName": "Assets History Tracker",
|
||||
"version": "1.0.0",
|
||||
"description": "Helper to track recently used assets in the editor and display them in a menu."
|
||||
}
|
||||
7
Packages/com.jovian.recentassets/package.json.meta
Normal file
7
Packages/com.jovian.recentassets/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74e2fe99929f7e84a92d5d34512b6942
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.jovian.utilties/Editor.meta
Normal file
8
Packages/com.jovian.utilties/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e079e01a6b0deb44865d02d9d276a3f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
144
Packages/com.jovian.utilties/Editor/CollisionExtractorEditor.cs
Normal file
144
Packages/com.jovian.utilties/Editor/CollisionExtractorEditor.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities.Editor {
|
||||
[CustomEditor(typeof(CollisionExtractor))]
|
||||
public class CollisionExtractorEditor : UnityEditor.Editor {
|
||||
|
||||
public override void OnInspectorGUI() {
|
||||
base.OnInspectorGUI();
|
||||
|
||||
serializedObject.Update();
|
||||
if(GUILayout.Button("Extract Colliders")) {
|
||||
ExtractCollidersIntoPrefab();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractCollidersIntoPrefab() {
|
||||
CollisionExtractor collisionExtractor = (CollisionExtractor)target;
|
||||
GameObject source = collisionExtractor.root;
|
||||
GameObject targetPrefab = collisionExtractor.targetPrefab;
|
||||
|
||||
// Verify input
|
||||
if(source == null) {
|
||||
Debug.LogWarning("[CollisionExtractor] no Root assigned, please assign a root then try again!");
|
||||
return;
|
||||
}
|
||||
if(targetPrefab == null) {
|
||||
Debug.LogWarning("[CollisionExtractor] no TargetPrefab assigned, please assign a root then try again!");
|
||||
return;
|
||||
}
|
||||
else if(targetPrefab.scene.name != null) {
|
||||
Debug.LogWarning("[CollisionExtractor] TargetPrefab is not a prefab instance, please assign a proper prefab instance!");
|
||||
return;
|
||||
}
|
||||
|
||||
string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(targetPrefab);
|
||||
GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
|
||||
|
||||
// Remove any old children and build up a new hierarchy matching the source
|
||||
prefabRoot.transform.DetachChildren();
|
||||
|
||||
// Try to remove all components on the prefab root (several iterations due to components sometime requiring each other, thus requiring a certain order of deletion)
|
||||
for(int i = 0; i < 6; i++) {
|
||||
foreach(var comp in prefabRoot.GetComponents<Component>()) {
|
||||
//Don't remove the Transform component
|
||||
if(!(comp is Transform)) {
|
||||
DestroyImmediate(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We make a copy of the source object to ensure it does not get changed, when accessing the properties using reflection Unity notes some of them as changed even though we only read from it.
|
||||
// For colliders this happens to the physics material, if it is set to "None" it will become an empty value on the source in the editor but properly pick the None value for the target.
|
||||
GameObject sourceCopy = Instantiate(source);
|
||||
try {
|
||||
CopyCollidersRecursive(sourceCopy, prefabRoot);
|
||||
}
|
||||
finally {
|
||||
DestroyImmediate(sourceCopy);
|
||||
}
|
||||
Debug.Log("[CollisionExtractor] extraction into target prefab complete.");
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
|
||||
PrefabUtility.UnloadPrefabContents(prefabRoot);
|
||||
}
|
||||
|
||||
private void CopyCollidersRecursive(GameObject sourceNode, GameObject targetNode) {
|
||||
// Copy all transform settings
|
||||
targetNode.transform.SetPositionAndRotation(sourceNode.transform.position, sourceNode.transform.rotation);
|
||||
targetNode.transform.localScale = sourceNode.transform.localScale;
|
||||
GameObjectUtility.SetStaticEditorFlags(targetNode, GameObjectUtility.GetStaticEditorFlags(sourceNode));
|
||||
targetNode.tag = sourceNode.tag;
|
||||
targetNode.layer = sourceNode.layer;
|
||||
|
||||
// Copy all collider components
|
||||
Collider[] colliders = sourceNode.GetComponents<Collider>();
|
||||
for(int i = 0; i < colliders.Length; ++i) {
|
||||
switch(colliders[i]) {
|
||||
case BoxCollider sourceBoxCollider:
|
||||
BoxCollider targetBoxCollider = targetNode.AddComponent<BoxCollider>();
|
||||
GetCopyOf(targetBoxCollider, sourceBoxCollider);
|
||||
break;
|
||||
case SphereCollider sourceSphereCollider:
|
||||
SphereCollider targetSphereCollider = targetNode.AddComponent<SphereCollider>();
|
||||
GetCopyOf(targetSphereCollider, sourceSphereCollider);
|
||||
break;
|
||||
case CapsuleCollider sourceCapsuleCollider:
|
||||
CapsuleCollider targetCapsuleCollider = targetNode.AddComponent<CapsuleCollider>();
|
||||
GetCopyOf(targetCapsuleCollider, sourceCapsuleCollider);
|
||||
break;
|
||||
case MeshCollider sourceMeshCollider:
|
||||
MeshCollider targetMeshCollider = targetNode.AddComponent<MeshCollider>();
|
||||
GetCopyOf(targetMeshCollider, sourceMeshCollider);
|
||||
break;
|
||||
default:
|
||||
Debug.LogError($"[CollisionExtractor] found unsupported collider type on game object {sourceNode.name}!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with all the child nodes
|
||||
for(int i = 0; i < sourceNode.transform.childCount; ++i) {
|
||||
Transform sourceChildNode = sourceNode.transform.GetChild(i);
|
||||
GameObject targetChildNode = new GameObject();
|
||||
targetChildNode.name = sourceChildNode.name;
|
||||
targetChildNode.transform.parent = targetNode.transform;
|
||||
CopyCollidersRecursive(sourceChildNode.gameObject, targetChildNode);
|
||||
}
|
||||
|
||||
// If we are a leaf node without colliders we are not useful, delete this node
|
||||
if(colliders.Length == 0 && targetNode.transform.childCount == 0) {
|
||||
DestroyImmediate(targetNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from https://answers.unity.com/questions/530178/how-to-get-a-component-from-an-object-and-add-it-t.html?_ga=2.50760041.112217741.1608192858-1956498980.1555671355
|
||||
private T GetCopyOf<T>(Component target, T source) where T : Component {
|
||||
Type type = target.GetType();
|
||||
if(type != source.GetType()) return null; // type mis-match
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default;
|
||||
var pinfos = from property in type.GetProperties(flags)
|
||||
where !property.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(ObsoleteAttribute))
|
||||
select property;
|
||||
foreach(var pinfo in pinfos) {
|
||||
if(pinfo.CanWrite) {
|
||||
try {
|
||||
pinfo.SetValue(target, pinfo.GetValue(source, null), null);
|
||||
}
|
||||
catch { } // In case of NotImplementedException being thrown. For some reason specifying that exception didn't seem to catch it, so I didn't catch anything specific.
|
||||
}
|
||||
}
|
||||
FieldInfo[] finfos = type.GetFields(flags);
|
||||
foreach(var finfo in finfos) {
|
||||
finfo.SetValue(target, finfo.GetValue(source));
|
||||
}
|
||||
return target as T;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 945d56cc182956e4a9dfb70d9051c0d1
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Jovian.Utilities.Editor {
|
||||
[CustomEditor(typeof(CustomRenderQueueMaterialList))]
|
||||
public class CustomRenderQueueMaterialListEditor : UnityEditor.Editor {
|
||||
|
||||
private const int MAXNAMELENGTH = 30;
|
||||
|
||||
public override void OnInspectorGUI() {
|
||||
if(((CustomRenderQueueMaterialList)target).updateMaterialsInEditor) {
|
||||
EditorGUILayout.HelpBox("Enabling 'updateMaterialsInEditor' will cause the Editor to change material files. Only use in debug and please review your changelist before submitting.", MessageType.Warning);
|
||||
}
|
||||
DrawAddGUI();
|
||||
base.OnInspectorGUI();
|
||||
DrawAddGUI();
|
||||
}
|
||||
|
||||
private void DrawAddGUI() {
|
||||
GUILayout.BeginHorizontal();
|
||||
if(GUILayout.Button("Add Empty")) {
|
||||
AddEmpty();
|
||||
}
|
||||
if(Selection.activeGameObject) {
|
||||
if(GUILayout.Button($"Add from '{GetNiceName(Selection.activeGameObject)}'")) {
|
||||
AddGameObject(Selection.activeGameObject);
|
||||
}
|
||||
}
|
||||
if(Selection.activeObject is Material) {
|
||||
if(GUILayout.Button($"Add '{GetNiceName(Selection.activeObject)}'")) {
|
||||
AddMaterial(Selection.activeObject as Material);
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private string GetNiceName(Object obj) {
|
||||
var objectName = obj.name;
|
||||
if(objectName.Length > MAXNAMELENGTH) {
|
||||
objectName = objectName.Substring(0, MAXNAMELENGTH - 2) + "..";
|
||||
}
|
||||
return objectName;
|
||||
}
|
||||
|
||||
private void AddEmpty() {
|
||||
AddElement(null, -1);
|
||||
}
|
||||
|
||||
private void AddGameObject(GameObject gameObject) {
|
||||
var renderer = gameObject.GetComponentInChildren<Renderer>();
|
||||
if(renderer == null) {
|
||||
Debug.LogError($"Cannot add Renderer from {gameObject}, there is none.");
|
||||
return;
|
||||
}
|
||||
AddMaterial(renderer.sharedMaterial);
|
||||
}
|
||||
|
||||
private void AddMaterial(Material material) {
|
||||
if(material == null) {
|
||||
Debug.LogError($"Cannot add Material, it is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(((CustomRenderQueueMaterialList)target).DoesMaterialExistInList(material)) {
|
||||
Debug.LogError($"Cannot add Material, it is already listed.");
|
||||
return;
|
||||
}
|
||||
|
||||
AddElement(material, material.renderQueue);
|
||||
}
|
||||
|
||||
private void AddElement(Material material, int renderQueue) {
|
||||
var listProperty = serializedObject.FindProperty("materialRenderQueueList");
|
||||
var count = listProperty.arraySize;
|
||||
listProperty.arraySize = count + 1;
|
||||
var elementProperty = listProperty.GetArrayElementAtIndex(count);
|
||||
var materialProperty = elementProperty.FindPropertyRelative("material");
|
||||
var renderQueueProperty = elementProperty.FindPropertyRelative("renderQueue");
|
||||
materialProperty.objectReferenceValue = material;
|
||||
renderQueueProperty.intValue = renderQueue;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe8e754720c57ef4089250664029c499
|
||||
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Jovian.Utilities.Editor {
|
||||
[CustomPropertyDrawer(typeof(CustomRenderQueueMaterialList.MaterialRenderQueue))]
|
||||
public class CustomRenderQueueMaterialListPropertyDrawer : PropertyDrawer {
|
||||
private const float RENDERQUEUE_PROPERTY_WIDTH = 45f;
|
||||
private const float GUI_PADDING = 2f;
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
|
||||
var indent = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
var materialRect = new Rect(position.x, position.y, position.width - RENDERQUEUE_PROPERTY_WIDTH, position.height);
|
||||
var renderQueueRect = new Rect(materialRect.xMax + GUI_PADDING, position.y, RENDERQUEUE_PROPERTY_WIDTH - GUI_PADDING, position.height);
|
||||
|
||||
EditorGUI.PropertyField(materialRect, property.FindPropertyRelative("material"), new GUIContent(string.Empty, "Material"));
|
||||
EditorGUI.PropertyField(renderQueueRect, property.FindPropertyRelative("renderQueue"), new GUIContent(string.Empty, "RenderQueue"));
|
||||
|
||||
EditorGUI.indentLevel = indent;
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5ca9116ca3b8724dad5a329325ff93d
|
||||
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Jovian.Utilities.Editor {
|
||||
public class NumberRangePropertyDrawer : PropertyDrawer {
|
||||
private const float PADDING = 1f;
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
|
||||
|
||||
float width = position.width * 0.5f - PADDING;
|
||||
Rect minRect = new Rect(position.x, position.y, width, position.height);
|
||||
Rect maxRect = new Rect(minRect.xMax + PADDING * 2f, position.y, width, position.height);
|
||||
|
||||
int indentLevel = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
EditorGUI.PropertyField(minRect, property.FindPropertyRelative("min"), GUIContent.none);
|
||||
EditorGUI.PropertyField(maxRect, property.FindPropertyRelative("max"), GUIContent.none);
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
|
||||
[CustomPropertyDrawer(typeof(FloatRange))]
|
||||
public class FloatRangePropertyDrawer : NumberRangePropertyDrawer { }
|
||||
|
||||
[CustomPropertyDrawer(typeof(IntRange))]
|
||||
public class IntRangePropertyDrawer : NumberRangePropertyDrawer { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 225b3cfe03be1ff41b3f72b06ae2fa23
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "JovianUtilities.Editor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"JovianUtilities"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9747d4f21927cf4c9f4a1c9c0680d86
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.jovian.utilties/Runtime.meta
Normal file
8
Packages/com.jovian.utilties/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0c62d31f10f1bb4ebd18d23cb9e50b7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
36
Packages/com.jovian.utilties/Runtime/ArrayUtility.cs
Normal file
36
Packages/com.jovian.utilties/Runtime/ArrayUtility.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public static class ArrayUtility {
|
||||
public static string ListToString(this IList list, bool newLinePerEntry = false) {
|
||||
if(list == null) {
|
||||
return "<NULL>";
|
||||
}
|
||||
StringBuilder sb = new();
|
||||
sb.Append("[");
|
||||
sb.Append(list.Count);
|
||||
sb.Append("]{");
|
||||
if(newLinePerEntry) {
|
||||
sb.AppendLine();
|
||||
}
|
||||
for(int i = 0, c = list.Count; i < c; i++) {
|
||||
sb.Append(list[i]);
|
||||
if(i < c - 1) {
|
||||
if(newLinePerEntry) {
|
||||
sb.AppendLine(",");
|
||||
}
|
||||
else {
|
||||
sb.Append(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(newLinePerEntry) {
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fff0c7e092c6f6a44ba792b369a382dd
|
||||
42
Packages/com.jovian.utilties/Runtime/BowserLog.cs
Normal file
42
Packages/com.jovian.utilties/Runtime/BowserLog.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public static class BowserLog {
|
||||
|
||||
private const string PREFIX = "Bowser:";
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), DebuggerHidden]
|
||||
public static void Log(string log, object obj = null) {
|
||||
Debug.Log(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), DebuggerHidden]
|
||||
public static void LogWarning(string log, object obj = null) {
|
||||
Debug.LogWarning(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), DebuggerHidden]
|
||||
public static void LogError(string log, object obj = null) {
|
||||
Debug.LogError(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD"), DebuggerHidden]
|
||||
public static void LogDebug(string log, object obj = null) {
|
||||
Debug.Log(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD"), DebuggerHidden]
|
||||
public static void LogWarningDebug(string log, object obj = null) {
|
||||
Debug.LogWarning(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD"), DebuggerHidden]
|
||||
public static void LogErrorDebug(string log, object obj = null) {
|
||||
Debug.LogError(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Packages/com.jovian.utilties/Runtime/BowserLog.cs.meta
Normal file
2
Packages/com.jovian.utilties/Runtime/BowserLog.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79607e8a014ff1841b183019d9102aff
|
||||
38
Packages/com.jovian.utilties/Runtime/CachedMainCamera.cs
Normal file
38
Packages/com.jovian.utilties/Runtime/CachedMainCamera.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
public static class CachedMainCamera {
|
||||
private static int lastFrame = -1;
|
||||
private static Camera mainCamera;
|
||||
private static Transform mainCameraTransform;
|
||||
|
||||
public static Camera MainCamera {
|
||||
get {
|
||||
if(mainCamera) {
|
||||
return mainCamera;
|
||||
}
|
||||
AssignCameraReferences();
|
||||
return mainCamera;
|
||||
}
|
||||
}
|
||||
|
||||
public static Transform MainCameraTransform {
|
||||
get {
|
||||
if(mainCameraTransform) {
|
||||
return mainCameraTransform;
|
||||
}
|
||||
AssignCameraReferences();
|
||||
return mainCameraTransform;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssignCameraReferences() {
|
||||
int frame = Time.frameCount;
|
||||
if(lastFrame != frame) {
|
||||
mainCamera = Camera.main;
|
||||
mainCameraTransform = (mainCamera ? mainCamera.transform : null);
|
||||
lastFrame = frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0701523b57a704b4780d0a226f0d0d1b
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
[RequireComponent(typeof(Canvas))]
|
||||
public class CanvasAutoAssignWorldCamera : MonoBehaviour {
|
||||
public Canvas canvas;
|
||||
public bool autoDisableOnceCameraFound = false;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void Reset() {
|
||||
SerializedObjectUtility.SaveObjectProperties(this, nameof(canvas), GetComponent<Canvas>());
|
||||
}
|
||||
#endif
|
||||
|
||||
private void Update() {
|
||||
if(canvas && !canvas.worldCamera) {
|
||||
canvas.worldCamera = CachedMainCamera.MainCamera;
|
||||
if(canvas.worldCamera && autoDisableOnceCameraFound) {
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59842e0a1599f854cae803d724ecb1e7
|
||||
99
Packages/com.jovian.utilties/Runtime/CollectionUtility.cs
Normal file
99
Packages/com.jovian.utilties/Runtime/CollectionUtility.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Jovian.Utilities.Utilities {
|
||||
public static class CollectionUtility {
|
||||
public static T RandomElementFromCollection<T>(this ICollection<T> enumerableObject) {
|
||||
int count = enumerableObject.Count;
|
||||
if (count == 0) {
|
||||
throw new IndexOutOfRangeException("Cannot get RandomElement, collection size is 0.");
|
||||
}
|
||||
|
||||
int index = Random.Range(0, count);
|
||||
return enumerableObject.ElementAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class EnumerableUtility {
|
||||
private static System.Random random;
|
||||
|
||||
public static T RandomElement<T>(this IEnumerable<T> source) {
|
||||
random ??= new System.Random();
|
||||
return source.RandomElement(random);
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/a/648240/584774
|
||||
public static T RandomElement<T>(this IEnumerable<T> source, System.Random rng) {
|
||||
T current = default(T);
|
||||
int count = 0;
|
||||
foreach (T element in source) {
|
||||
count++;
|
||||
if (rng.Next(count) == 0) {
|
||||
current = element;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
throw new InvalidOperationException("Sequence was empty");
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public static bool TryGetRandomElement<T>(this IEnumerable<T> source, out T outElement) {
|
||||
random ??= new System.Random();
|
||||
return source.TryGetRandomElement(random, out outElement);
|
||||
}
|
||||
|
||||
public static bool TryGetRandomElement<T>(this IEnumerable<T> source, System.Random rng, out T outElement) {
|
||||
T current = default(T);
|
||||
int count = 0;
|
||||
foreach (T element in source) {
|
||||
count++;
|
||||
if (rng.Next(count) == 0) {
|
||||
current = element;
|
||||
}
|
||||
}
|
||||
|
||||
outElement = current;
|
||||
if (count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string EnumerableToString(this IEnumerable enumerable, bool newLinePerEntry = false) {
|
||||
if (enumerable == null) {
|
||||
return "<NULL>";
|
||||
}
|
||||
|
||||
StringBuilder sb = new();
|
||||
sb.Append("{");
|
||||
if (newLinePerEntry) {
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
foreach (object item in enumerable) {
|
||||
sb.Append(item);
|
||||
if (newLinePerEntry) {
|
||||
sb.AppendLine(",");
|
||||
}
|
||||
else {
|
||||
sb.Append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (newLinePerEntry) {
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a6194020ff13054597ed18112a7acff
|
||||
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
public static class ColliderUtilities {
|
||||
public static bool ContainsPoint(this Collider collider, Vector3 point) {
|
||||
return (collider.ClosestPoint(point) - point).sqrMagnitude < Mathf.Epsilon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 033c7401511b25948be41b7ab4774a03
|
||||
18
Packages/com.jovian.utilties/Runtime/CollisionExtractor.cs
Normal file
18
Packages/com.jovian.utilties/Runtime/CollisionExtractor.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
public class CollisionExtractor : MonoBehaviour {
|
||||
|
||||
public GameObject root;
|
||||
public GameObject targetPrefab;
|
||||
|
||||
// Script only used inside the editor
|
||||
private void Awake() {
|
||||
if(!Application.isEditor) {
|
||||
Destroy(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1db2adf065c60df4d89c324e77eba81d
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public class CustomRenderQueueMaterialList : MonoBehaviour {
|
||||
|
||||
[System.Serializable]
|
||||
public class MaterialRenderQueue {
|
||||
public Material material;
|
||||
public int renderQueue;
|
||||
|
||||
public int storedRenderQueue;
|
||||
}
|
||||
|
||||
public bool updateMaterialsInEditor;
|
||||
[SerializeField]
|
||||
private MaterialRenderQueue[] materialRenderQueueList;
|
||||
|
||||
private void Awake() {
|
||||
#if UNITY_EDITOR
|
||||
if(updateMaterialsInEditor) {
|
||||
Debug.LogWarning("Updating Materials will cause asset files to change. Please review your change log to ensure only valid changes are submitted.");
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach(var materialRenderQueue in materialRenderQueueList) {
|
||||
if(materialRenderQueue.material) {
|
||||
materialRenderQueue.storedRenderQueue = materialRenderQueue.material.renderQueue; // store the materials original render queue
|
||||
materialRenderQueue.material.renderQueue = materialRenderQueue.renderQueue; // overwrite the render queue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
#if UNITY_EDITOR
|
||||
if(updateMaterialsInEditor) {
|
||||
Debug.LogWarning("Updating Materials will cause asset files to change. Please review your change log to ensure only valid changes are submitted.");
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
foreach(var materialRenderQueue in materialRenderQueueList) {
|
||||
if(materialRenderQueue.material) {
|
||||
materialRenderQueue.material.renderQueue = materialRenderQueue.storedRenderQueue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool DoesMaterialExistInList(Material material) {
|
||||
foreach(var materialRenderQueue in materialRenderQueueList) {
|
||||
if(materialRenderQueue.material == material) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a49f06b9d6f75f04fa4ae4f6ee714a83
|
||||
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public class CustomRenderQueueRenderer : MonoBehaviour {
|
||||
[SerializeField, Tooltip("Override RenderQueue for shader, 2000 = Opaque, 3000 = Transparent, -1 = Shader Default")]
|
||||
private int renderQueue = -1;
|
||||
[SerializeField]
|
||||
private MeshRenderer meshRenderer;
|
||||
[SerializeField, Tooltip("If true, only this renderer is affected because a new material is created.")]
|
||||
private bool createMaterialInstance;
|
||||
|
||||
private void Awake() {
|
||||
var material = createMaterialInstance ? meshRenderer.material : meshRenderer.sharedMaterial;
|
||||
material.renderQueue = renderQueue;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void Reset() {
|
||||
var serializedObject = new UnityEditor.SerializedObject(this);
|
||||
var meshRendererProperty = serializedObject.FindProperty("meshRenderer");
|
||||
var meshRenderer = gameObject.GetComponent<MeshRenderer>();
|
||||
if(meshRenderer != null) {
|
||||
meshRendererProperty.objectReferenceValue = meshRenderer;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
CopyRenderQueueFromMaterial(); // applies serializedObject modified properties
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Copy RenderQueue from Material")]
|
||||
private void CopyRenderQueueFromMaterial() {
|
||||
if(meshRenderer == null) {
|
||||
Debug.LogError(@"MeshRenderer is null, cannot copy RenderQueue", this);
|
||||
return;
|
||||
}
|
||||
var serializedObject = new UnityEditor.SerializedObject(this);
|
||||
var renderQueueProperty = serializedObject.FindProperty("renderQueue");
|
||||
renderQueueProperty.intValue = meshRenderer.sharedMaterial.renderQueue;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c20f17639e020c0448b1481fad636678
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45fad90effb685541beabcc012dca7f5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Debug = UnityEngine.Debug;
|
||||
using Object = UnityEngine.Object;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
public static class AssetUtility {
|
||||
|
||||
public static TAsset FindAssetInProject<TAsset>(string assetName = "") where TAsset : Object {
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:{typeof(TAsset).Name} {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
return asset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return default(TAsset);
|
||||
}
|
||||
|
||||
public static Object FindAssetInProject(Type assetType, string assetName = "") {
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:{assetType.Name} {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
return asset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return default(Object);
|
||||
}
|
||||
|
||||
public static List<TAsset> FindAllAssetsInProject<TAsset>(string assetName = "") where TAsset : Object {
|
||||
var list = new List<TAsset>();
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:{typeof(TAsset).Name} {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
list.Add(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<Object> FindAllAssetsInProject(Type assetType, string assetName = "") {
|
||||
var list = new List<Object>();
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:{assetType.Name} {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
list.Add(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<Object> FindAllObjectsInProject(Type objectType, string filter) {
|
||||
List<Object> list = new List<Object>();
|
||||
#if UNITY_EDITOR
|
||||
string[] guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
Object asset = AssetDatabase.LoadAssetAtPath<Object>(path);
|
||||
|
||||
if (typeof(Component).IsAssignableFrom(objectType) &&
|
||||
TryGetTypeObjectWithPrefab(asset, objectType, out Component _))
|
||||
{
|
||||
list.Add(asset);
|
||||
}
|
||||
else if (asset.GetType().IsAssignableFrom(objectType))
|
||||
{
|
||||
list.Add(asset);
|
||||
}
|
||||
else if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return list;
|
||||
}
|
||||
|
||||
// Prefabs
|
||||
public static TAsset FindPrefabInProject<TAsset>(string assetName = "") where TAsset : Component {
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:Prefab {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
|
||||
if (TryGetTypeObjectWithPrefab(asset, out TAsset component))
|
||||
{
|
||||
return component;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
Debug.LogError($"Failed to find asset '{assetName}' <{typeof(TAsset)}>");
|
||||
return default(TAsset);
|
||||
}
|
||||
|
||||
public static Object FindPrefabInProject(Type assetType, string assetName = "") {
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:Prefab {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
|
||||
if (TryGetTypeObjectWithPrefab(asset, assetType, out Component component))
|
||||
{
|
||||
return component;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return default(Object);
|
||||
}
|
||||
|
||||
public static List<TAsset> FindAllPrefabsInProject<TAsset>(string assetName = "") where TAsset : Component {
|
||||
var list = new List<TAsset>();
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:Prefab {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
|
||||
if (TryGetTypeObjectWithPrefab(asset, out TAsset component))
|
||||
{
|
||||
list.Add(component);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<Object> FindAllPrefabsInProject(Type assetType, string assetName = "") {
|
||||
var list = new List<Object>();
|
||||
#if UNITY_EDITOR
|
||||
var filter = $"t:Prefab {assetName}";
|
||||
var guids = AssetDatabase.FindAssets(filter);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
|
||||
if (TryGetTypeObjectWithPrefab(asset, assetType, out Component component))
|
||||
{
|
||||
list.Add(component);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldUnloadAsset(asset))
|
||||
{
|
||||
Resources.UnloadAsset(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
Debug.LogError("AssetUtility should not be called in non-Editor mode");
|
||||
#endif
|
||||
return list;
|
||||
}
|
||||
|
||||
// Util
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static bool ShouldUnloadAsset(Object asset) {
|
||||
if (asset == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(asset is GameObject or Component or AssetBundle ||
|
||||
PrefabUtility.GetPrefabAssetType(asset) != PrefabAssetType.NotAPrefab);
|
||||
}
|
||||
|
||||
private static bool TryGetTypeObjectWithPrefab(Object prefab, Type type, out Component component) {
|
||||
if (prefab != null && prefab is GameObject gameObject && gameObject.TryGetComponent(type, out component))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
component = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetTypeObjectWithPrefab<TComponent>(Object prefab, out TComponent component)
|
||||
where TComponent : Component {
|
||||
if (prefab != null && prefab is TComponent prefabAsComponent)
|
||||
{
|
||||
component = prefabAsComponent;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prefab != null && prefab is GameObject gameObject && gameObject.TryGetComponent(out component))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
component = default;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f53e63ec5b4b3744bda48fb1159ac40
|
||||
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
/// <summary>
|
||||
/// <b>Editor use-only.</b><br/>
|
||||
/// Allows easy access between different instances. Supports 1 instance per type. <br/>
|
||||
/// Add instances and retrieve elsewhere.<br/><br/>
|
||||
/// Read more: https://en.wikipedia.org/wiki/Service_locator_pattern
|
||||
/// </summary>
|
||||
public static class EditorServiceLocator {
|
||||
private sealed class ServiceNotFoundException : Exception {
|
||||
public ServiceNotFoundException(Type type) : base($"ServiceNotFoundException. Type={type}") { }
|
||||
}
|
||||
|
||||
private static Dictionary<Type, object> serviceContainer = new();
|
||||
public static bool IsDirty { get; private set; }
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static Dictionary<Type, object> ServiceContainer => serviceContainer;
|
||||
#endif
|
||||
|
||||
[RuntimeInitializeOnLoadMethod]
|
||||
public static void Init() {
|
||||
serviceContainer = new();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[InitializeOnEnterPlayMode]
|
||||
private static void Reset(EnterPlayModeOptions options) {
|
||||
serviceContainer?.Clear();
|
||||
IsDirty = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Add an instance to the locator. Will throw an exception if a type already exists in the locator.
|
||||
/// <seealso cref="AddOrReplace{T}"/>
|
||||
/// </summary>
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void Add<T>(T service) {
|
||||
#if UNITY_EDITOR
|
||||
if (service == null) {
|
||||
throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
|
||||
}
|
||||
|
||||
serviceContainer.Add(typeof(T), service);
|
||||
IsDirty = true;
|
||||
#else
|
||||
UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an instance to the locator. Will replace any existing instance without an exception.
|
||||
/// An alias for <see cref="Set{T}"/>
|
||||
/// </summary>
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void AddOrReplace<T>(T service) {
|
||||
Set(service);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an instance to the locator. Will replace any existing instance without an exception.
|
||||
/// An alias for <see cref="AddOrReplace{T}"/>
|
||||
/// </summary>
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void Set<T>(T service) {
|
||||
#if UNITY_EDITOR
|
||||
if (service == null) {
|
||||
throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
|
||||
}
|
||||
|
||||
serviceContainer[typeof(T)] = service;
|
||||
IsDirty = true;
|
||||
#else
|
||||
UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes any type matching the instance passed in from the locator. This is good practice.
|
||||
/// </summary>
|
||||
/// <param name="service"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <exception cref="NullReferenceException"></exception>
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void Remove<T>(T service) {
|
||||
#if UNITY_EDITOR
|
||||
if (service == null) {
|
||||
throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
|
||||
}
|
||||
|
||||
serviceContainer.Remove(typeof(T));
|
||||
IsDirty = true;
|
||||
#else
|
||||
UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an instance from the locator matching the type. Will throw an exception if nothing is found.
|
||||
/// <seealso cref="TryGet{T}(out T)"/>
|
||||
/// </summary>
|
||||
public static T Get<T>() {
|
||||
#if UNITY_EDITOR
|
||||
if (serviceContainer.TryGetValue(typeof(T), out object service)) {
|
||||
return (T)service;
|
||||
}
|
||||
#endif
|
||||
throw new ServiceNotFoundException(typeof(T));
|
||||
}
|
||||
|
||||
[Obsolete("Use Get<T> or TryGet<T>(out T) since they follow the C# conventions for TryGet")]
|
||||
public static T TryGet<T>() {
|
||||
#if UNITY_EDITOR
|
||||
if (serviceContainer.TryGetValue(typeof(T), out object service)) {
|
||||
return (T)service;
|
||||
}
|
||||
#endif
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an instance from the locator matching the type. Returns true/false based on success.
|
||||
/// </summary>
|
||||
public static bool TryGet<T>(out T instance) {
|
||||
#if UNITY_EDITOR
|
||||
if (serviceContainer.TryGetValue(typeof(T), out object service)) {
|
||||
instance = (T)service;
|
||||
return instance != null;
|
||||
}
|
||||
#endif
|
||||
instance = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void Clear() {
|
||||
serviceContainer.Clear();
|
||||
IsDirty = true;
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void Clean() {
|
||||
IsDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8316472f7c700f4a9e908d3abde1d1d
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Jovian.Utilities.Utilities {
|
||||
public static class GizmosUtility {
|
||||
|
||||
public static bool IsGameObjectOrChildSelected(GameObject gameObject) {
|
||||
#if UNITY_EDITOR
|
||||
return GameObjectUtilities.IsGameObjectAChildOfGameObject(Selection.activeGameObject, gameObject);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void DrawColliders(IEnumerable<Collider> colliders) {
|
||||
foreach(Collider collider in colliders) {
|
||||
if(collider != null) {
|
||||
DrawCollider(collider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawWireColliders(IEnumerable<Collider> colliders) {
|
||||
foreach(Collider collider in colliders) {
|
||||
if(collider != null) {
|
||||
DrawWireCollider(collider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawCollider(Collider collider)
|
||||
=> DrawColliderInternal(collider, Gizmos.DrawSphere, Gizmos.DrawCube, Gizmos.DrawMesh);
|
||||
|
||||
public static void DrawWireCollider(Collider collider)
|
||||
=> DrawColliderInternal(collider, Gizmos.DrawWireSphere, Gizmos.DrawWireCube, Gizmos.DrawWireMesh);
|
||||
|
||||
private static void DrawColliderInternal(Collider collider, Action<Vector3, float> drawSphere, Action<Vector3, Vector3> drawCube,
|
||||
Action<Mesh> drawMesh) {
|
||||
Gizmos.matrix = collider.transform.localToWorldMatrix;
|
||||
switch(collider) {
|
||||
case BoxCollider boxCollider:
|
||||
drawCube(boxCollider.center, boxCollider.size);
|
||||
break;
|
||||
case SphereCollider sphereCollider:
|
||||
drawSphere(sphereCollider.center, sphereCollider.radius);
|
||||
break;
|
||||
case CapsuleCollider capsuleCollider: {
|
||||
Vector3 direction = GetAxis(capsuleCollider.direction);
|
||||
drawSphere(capsuleCollider.center + direction * capsuleCollider.height * 0.5f, capsuleCollider.radius);
|
||||
drawSphere(capsuleCollider.center - direction * capsuleCollider.height * 0.5f, capsuleCollider.radius);
|
||||
break;
|
||||
}
|
||||
case MeshCollider meshCollider:
|
||||
drawMesh(meshCollider.sharedMesh);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Cannot draw collider of type '{typeof(Collider)}'");
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 GetAxis(int direction) {
|
||||
switch(direction) {
|
||||
case 0:
|
||||
return Vector3.right;
|
||||
case 1:
|
||||
return Vector3.up;
|
||||
case 2:
|
||||
return Vector3.forward;
|
||||
default:
|
||||
throw new NotSupportedException($"Direction '{direction}' does not map to an axis.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2ce31502182718488f21bcf6137096f
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor.SceneManagement;
|
||||
#endif
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
public static class HierarchyUtility {
|
||||
#if UNITY_EDITOR
|
||||
public static List<TComponent> FindComponentsInHierarchy<TComponent>(bool includeInactive = false) where TComponent : Component {
|
||||
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if(prefabStage == null) {
|
||||
return SceneUtility.FindComponentsInActiveScene<TComponent>(includeInactive);
|
||||
}
|
||||
else {
|
||||
return new List<TComponent>(prefabStage.prefabContentsRoot.GetComponentsInChildren<TComponent>(includeInactive));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Component> FindComponentsInHierarchy(Type componentType, bool includeInactive = false) {
|
||||
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if(prefabStage == null) {
|
||||
return SceneUtility.FindComponentsInActiveScene(componentType, includeInactive);
|
||||
}
|
||||
else {
|
||||
return new List<Component>(prefabStage.prefabContentsRoot.GetComponentsInChildren(componentType, includeInactive));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fce733b284594854aaf7c9720f440333
|
||||
@@ -0,0 +1,68 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public static class InspectorGUIUtility {
|
||||
public static object DrawField(string name, Type type, object value) {
|
||||
GUIContent label = new(name, $"<{type.Name}> = {value}");
|
||||
if(type == typeof(string)) {
|
||||
return EditorGUILayout.TextField(label, (string)value);
|
||||
}
|
||||
if(type == typeof(bool)) {
|
||||
return EditorGUILayout.Toggle(label, (bool)value);
|
||||
}
|
||||
if(type == typeof(float)) {
|
||||
return EditorGUILayout.FloatField(label, (float)value);
|
||||
}
|
||||
if(type == typeof(int)) {
|
||||
return EditorGUILayout.IntField(label, (int)value);
|
||||
}
|
||||
if(type == typeof(byte)) {
|
||||
return (byte)EditorGUILayout.IntField(label, (byte)value);
|
||||
}
|
||||
if(type == typeof(Vector2)) {
|
||||
return EditorGUILayout.Vector2Field(label, (Vector2)value);
|
||||
}
|
||||
if(type == typeof(Vector3)) {
|
||||
return EditorGUILayout.Vector3Field(label, (Vector3)value);
|
||||
}
|
||||
if(type == typeof(Vector4)) {
|
||||
return EditorGUILayout.Vector4Field(label, (Vector4)value);
|
||||
}
|
||||
if(type == typeof(Bounds)) {
|
||||
return EditorGUILayout.BoundsField(label, (Bounds)value);
|
||||
}
|
||||
if(type == typeof(BoundsInt)) {
|
||||
return EditorGUILayout.BoundsIntField(label, (BoundsInt)value);
|
||||
}
|
||||
if(type == typeof(Color)) {
|
||||
return EditorGUILayout.ColorField(label, (Color)value);
|
||||
}
|
||||
if(type == typeof(AnimationCurve)) {
|
||||
return EditorGUILayout.CurveField(label, (AnimationCurve)value);
|
||||
}
|
||||
if (type == typeof(double)) {
|
||||
return EditorGUILayout.DoubleField(label, (double)value);
|
||||
}
|
||||
if (type == typeof(Gradient)) {
|
||||
return EditorGUILayout.GradientField(label, (Gradient)value);
|
||||
}
|
||||
if (type == typeof(long)) {
|
||||
return EditorGUILayout.LongField(label, (long)value);
|
||||
}
|
||||
if(type.IsEnum) {
|
||||
return EditorGUILayout.EnumPopup(label, (Enum)value);
|
||||
}
|
||||
if(type.IsSubclassOf(typeof(UnityEngine.Object)) || value is UnityEngine.Object) {
|
||||
return EditorGUILayout.ObjectField(label, (UnityEngine.Object)value, type, true);
|
||||
}
|
||||
string stringValue = value == null ? "<NULL>" : value.ToString();
|
||||
EditorGUILayout.TextField(label, stringValue);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b62c711143ba3e4a8d4e941f00a8d2c
|
||||
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
public static class SceneUtility {
|
||||
|
||||
public static List<GameObject> FindGameObjectsInScene(this Scene scene, bool includeInactive = false, Func<GameObject, bool> predicate = null) {
|
||||
if (scene.IsValid() == false)
|
||||
{
|
||||
Debug.LogError("Scene is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new List<GameObject>();
|
||||
GameObject[] sceneObjects = scene.GetRootGameObjects();
|
||||
GameObject[] loadedObjects = FindDontDestroyOnLoadObjects();
|
||||
GameObject[] combinedGameobjects = new GameObject[sceneObjects.Length + loadedObjects.Length];
|
||||
sceneObjects.CopyTo(combinedGameobjects, 0);
|
||||
loadedObjects.CopyTo(combinedGameobjects, sceneObjects.Length);
|
||||
foreach (var rootObject in combinedGameobjects)
|
||||
{
|
||||
var allChildTransforms = rootObject.GetComponentsInChildren<Transform>(includeInactive);
|
||||
var allGameObjects = allChildTransforms.Select(transform => transform.gameObject);
|
||||
if (predicate != null)
|
||||
{
|
||||
allGameObjects = allGameObjects.Where(predicate);
|
||||
}
|
||||
|
||||
list.AddRange(allGameObjects);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<GameObject> FindGameObjectsInActiveScene(bool includeInactive = false, Func<GameObject, bool> predicate = null)
|
||||
=> FindGameObjectsInScene(SceneManager.GetActiveScene(), includeInactive, predicate);
|
||||
|
||||
// Adding support for finding objects not in the main scenes but in DontDestroyOnLoad
|
||||
// From https://forum.unity.com/threads/editor-script-how-to-access-objects-under-dontdestroyonload-while-in-play-mode.442014/#post-3570916
|
||||
private static GameObject[] FindDontDestroyOnLoadObjects() {
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
return new GameObject[] { }; // return an empty array as this method creates issues in edit mode
|
||||
}
|
||||
|
||||
GameObject temp = null;
|
||||
try
|
||||
{
|
||||
temp = new GameObject();
|
||||
Object.DontDestroyOnLoad(temp);
|
||||
UnityEngine.SceneManagement.Scene dontDestroyOnLoad = temp.scene;
|
||||
Object.DestroyImmediate(temp);
|
||||
temp = null;
|
||||
|
||||
return dontDestroyOnLoad.GetRootGameObjects();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (temp != null)
|
||||
Object.DestroyImmediate(temp);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<TComponent> FindComponentsInScene<TComponent>(this Scene scene, bool includeInactive = false) where TComponent : Component {
|
||||
if (scene.IsValid() == false)
|
||||
{
|
||||
Debug.LogError("Scene is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new List<TComponent>();
|
||||
|
||||
foreach (var rootObject in scene.GetRootGameObjects())
|
||||
{
|
||||
list.AddRange(rootObject.GetComponentsInChildren<TComponent>(includeInactive));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<Component> FindComponentsInScene(this Scene scene, Type componentType, bool includeInactive = false) {
|
||||
if (scene.IsValid() == false)
|
||||
{
|
||||
Debug.LogError("Scene is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new List<Component>();
|
||||
foreach (GameObject rootObject in scene.GetRootGameObjects())
|
||||
{
|
||||
list.AddRange(rootObject.GetComponentsInChildren(componentType, includeInactive));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<TComponent> FindComponentsInActiveScene<TComponent>(bool includeInactive = false) where TComponent : Component
|
||||
=> FindComponentsInScene<TComponent>(SceneManager.GetActiveScene(), includeInactive);
|
||||
|
||||
public static List<Component> FindComponentsInActiveScene(Type componentType, bool includeInactive = false)
|
||||
=> FindComponentsInScene(SceneManager.GetActiveScene(), componentType, includeInactive);
|
||||
|
||||
public static List<TComponent> FindComponentsInAllScenes<TComponent>(bool includeInactive = false) where TComponent : Component {
|
||||
List<TComponent> allComponents = new();
|
||||
for (int i = 0, c = SceneManager.sceneCount; i < c; i++)
|
||||
{
|
||||
allComponents.AddRange(FindComponentsInScene<TComponent>(SceneManager.GetSceneAt(i), includeInactive));
|
||||
}
|
||||
|
||||
return allComponents;
|
||||
}
|
||||
|
||||
public static TComponent FindComponentInScene<TComponent>(this Scene scene, bool includeInactive = false) where TComponent : Component {
|
||||
if (scene.IsValid() == false)
|
||||
{
|
||||
Debug.LogError("Scene is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new List<TComponent>();
|
||||
|
||||
foreach (var rootObject in scene.GetRootGameObjects())
|
||||
{
|
||||
list.AddRange(rootObject.GetComponentsInChildren<TComponent>(includeInactive));
|
||||
}
|
||||
|
||||
return list.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static Component FindComponentInScene(this Scene scene, Type componentType, bool includeInactive = false) {
|
||||
if (scene.IsValid() == false)
|
||||
{
|
||||
Debug.LogError("Scene is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new List<Component>();
|
||||
|
||||
foreach (GameObject rootObject in scene.GetRootGameObjects())
|
||||
{
|
||||
list.AddRange(rootObject.GetComponentsInChildren(componentType, includeInactive));
|
||||
}
|
||||
|
||||
return list.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static TComponent FindComponentInActiveScene<TComponent>(bool includeInactive = false) where TComponent : Component
|
||||
=> FindComponentInScene<TComponent>(SceneManager.GetActiveScene(), includeInactive);
|
||||
|
||||
public static Component FindComponentInActiveScene(Type componentType, bool includeInactive = false)
|
||||
=> FindComponentInScene(SceneManager.GetActiveScene(), componentType, includeInactive);
|
||||
|
||||
public static TComponent FindComponentInAllScenes<TComponent>(bool includeInactive = false) where TComponent : Component
|
||||
=> FindComponentsInAllScenes<TComponent>(includeInactive).FirstOrDefault();
|
||||
|
||||
|
||||
public static GameObject FindGameObject(string nameQuery) {
|
||||
char delimiter = '/';
|
||||
string[] querySegments = nameQuery.Split(delimiter);
|
||||
|
||||
return FindGameObjectsInActiveScene(true, (gameObject) => {
|
||||
int queryIndex = querySegments.Length - 1;
|
||||
Transform compareTransform = gameObject.transform;
|
||||
do
|
||||
{
|
||||
if (compareTransform != null && compareTransform.name.Equals(querySegments[queryIndex]))
|
||||
{
|
||||
compareTransform = compareTransform.parent;
|
||||
queryIndex--;
|
||||
}
|
||||
// if compare is null and string is null that means the first query segment was /, so that's the root element - then our parent MUST be null
|
||||
else if (compareTransform == null && string.IsNullOrEmpty(querySegments[queryIndex]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
} while (queryIndex >= 0);
|
||||
|
||||
return true;
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static Transform GetChildWithName(this Transform transform, string childName) {
|
||||
foreach (Transform child in transform.transform)
|
||||
{
|
||||
if (child.name.Equals(childName))
|
||||
{
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2e17c945fb193744abb8d14866b11c0
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
using Object = UnityEngine.Object;
|
||||
using Type = System.Type;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using System.Reflection;
|
||||
#endif
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public static class SerializedObjectUtility {
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
public static void SaveObjectProperties(Object targetObject, params object[] args) {
|
||||
#if UNITY_EDITOR
|
||||
EditorSerializedObjectUtility.SaveObjectProperties(targetObject, args);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
public static class EditorSerializedObjectUtility {
|
||||
|
||||
//https://answers.unity.com/questions/929293/get-field-type-of-serializedproperty.html
|
||||
public static Type GetTypeFromProperty(SerializedProperty property) {
|
||||
//gets parent type info
|
||||
string[] slices = property.propertyPath.Split('.');
|
||||
System.Type type = property.serializedObject.targetObject.GetType();
|
||||
|
||||
for(int i = 0; i < slices.Length; i++)
|
||||
if (slices[i] == "Array")
|
||||
{
|
||||
i++; //skips "data[x]"
|
||||
type = type.GetElementType(); //gets info on array elements
|
||||
}
|
||||
|
||||
//gets info on field and its type
|
||||
else type = type.GetField(slices[i], BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance).FieldType;
|
||||
|
||||
//type is now the type of the property
|
||||
return type;
|
||||
}
|
||||
|
||||
private static readonly Regex isArrayElementRegex = new Regex("Array.data\\[\\d+\\]$");
|
||||
|
||||
public static bool IsPropertyAnArrayElement(SerializedProperty property) {
|
||||
return isArrayElementRegex.IsMatch(property.propertyPath);
|
||||
}
|
||||
|
||||
public static SerializedProperty GetArrayPropertyWithElementProperty(SerializedProperty property) {
|
||||
SerializedObject serializedObject = property.serializedObject;
|
||||
string propertyPath = property.propertyPath;
|
||||
int arrayDataIndex = propertyPath.LastIndexOf(".Array.data", StringComparison.Ordinal);
|
||||
string propertyPathWithoutArray = propertyPath.Substring(0, arrayDataIndex);
|
||||
int pathDividerIndex = propertyPathWithoutArray.LastIndexOf(".", StringComparison.Ordinal);
|
||||
|
||||
string parentPropertyName = propertyPathWithoutArray;
|
||||
if(pathDividerIndex != -1) {
|
||||
parentPropertyName = propertyPathWithoutArray.Substring(pathDividerIndex);
|
||||
}
|
||||
return serializedObject.FindProperty(parentPropertyName);;
|
||||
}
|
||||
|
||||
public static void SaveObjectProperties(Object targetObject, params object[] args) {
|
||||
SerializedObject serializedObject = new SerializedObject(targetObject);
|
||||
|
||||
for(int i = 0; i < args.Length; i += 2) {
|
||||
var keyArg = args[i];
|
||||
var keyType = keyArg.GetType();
|
||||
|
||||
if((keyType == typeof(string)) == false) {
|
||||
throw new System.NotSupportedException(string.Format("Key must be string. {0} is {1}", args[i], keyType));
|
||||
}
|
||||
else {
|
||||
var property = serializedObject.FindProperty((string)keyArg);
|
||||
object argValue = args[i + 1];
|
||||
|
||||
if(property == null) {
|
||||
throw new System.Exception(string.Format("No property found for key {0}", keyArg));
|
||||
}
|
||||
|
||||
if(argValue == null) {
|
||||
property.objectReferenceValue = null;
|
||||
}
|
||||
else {
|
||||
if(property.isArray) {
|
||||
property.arraySize = 0;
|
||||
IEnumerable argArray = (IEnumerable)argValue;
|
||||
IEnumerator enumerator = argArray.GetEnumerator();
|
||||
|
||||
int index = 0;
|
||||
while(enumerator.MoveNext()) {
|
||||
property.InsertArrayElementAtIndex(index);
|
||||
var arrayProperty = property.GetArrayElementAtIndex(index);
|
||||
SetSerializedPropertyValue(arrayProperty, enumerator.Current);
|
||||
index++;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
SetSerializedPropertyValue(property, argValue);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private static void SetSerializedPropertyValue(SerializedProperty property, object value) {
|
||||
if(property == null) {
|
||||
UnityEngine.Debug.LogError("SetSerializedPropertyValue failed, property is null");
|
||||
return;
|
||||
}
|
||||
switch(property.propertyType) {
|
||||
case SerializedPropertyType.AnimationCurve:
|
||||
property.animationCurveValue = (AnimationCurve)value;
|
||||
break;
|
||||
case SerializedPropertyType.Boolean:
|
||||
property.boolValue = (bool)value;
|
||||
break;
|
||||
case SerializedPropertyType.BoundsInt:
|
||||
property.boundsIntValue = (BoundsInt)value;
|
||||
break;
|
||||
case SerializedPropertyType.Character:
|
||||
property.intValue = (int)(char)value;
|
||||
break;
|
||||
case SerializedPropertyType.Color: {
|
||||
if(value is Color32) {
|
||||
property.colorValue = (Color)(Color32)value;
|
||||
}
|
||||
else {
|
||||
property.colorValue = (Color)value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SerializedPropertyType.ExposedReference:
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
property.objectReferenceValue = (Object)value;
|
||||
break;
|
||||
case SerializedPropertyType.Float:
|
||||
property.floatValue = (float)value;
|
||||
break;
|
||||
case SerializedPropertyType.Integer:
|
||||
property.intValue = (int)value;
|
||||
break;
|
||||
case SerializedPropertyType.LayerMask:
|
||||
property.intValue = ((LayerMask)value).value;
|
||||
break;
|
||||
case SerializedPropertyType.Quaternion:
|
||||
property.quaternionValue = (Quaternion)value;
|
||||
break;
|
||||
case SerializedPropertyType.Rect:
|
||||
property.rectValue = (Rect)value;
|
||||
break;
|
||||
case SerializedPropertyType.RectInt:
|
||||
property.rectIntValue = (RectInt)value;
|
||||
break;
|
||||
case SerializedPropertyType.String:
|
||||
property.stringValue = (string)value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector2:
|
||||
property.vector2Value = (Vector2)value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector2Int:
|
||||
property.vector2IntValue = (Vector2Int)value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector3:
|
||||
property.vector3Value = (Vector3)value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector3Int:
|
||||
property.vector3IntValue = (Vector3Int)value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector4:
|
||||
property.vector4Value = (Vector4)value;
|
||||
break;
|
||||
case SerializedPropertyType.Enum:
|
||||
property.enumValueIndex = (int)value; // need to test this
|
||||
// flags???
|
||||
break;
|
||||
case SerializedPropertyType.Generic:
|
||||
SaveGenericProperty(property, value);
|
||||
break;
|
||||
default:
|
||||
throw new System.NotSupportedException($"PropertyType {property.propertyType} is not supported - yet. Array? {property.isArray} Path: {property.propertyPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveGenericProperty(SerializedProperty property, object instance) {
|
||||
Type type = instance.GetType();
|
||||
var fields = type.GetRuntimeFields();
|
||||
foreach(FieldInfo field in fields) {
|
||||
if(field.IsNotSerialized || field.IsStatic) {
|
||||
continue;
|
||||
}
|
||||
SerializedProperty fieldProperty = property.FindPropertyRelative(field.Name);
|
||||
if(fieldProperty != null) {
|
||||
SetSerializedPropertyValue(property.FindPropertyRelative(field.Name), field.GetValue(instance));
|
||||
}
|
||||
else {
|
||||
UnityEngine.Debug.Log($"SaveGenericProperty cannot field serializedProperty named '{field.Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51917079be732084cbaae41d702e72ce
|
||||
45
Packages/com.jovian.utilties/Runtime/FloatRange.cs
Normal file
45
Packages/com.jovian.utilties/Runtime/FloatRange.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Utilities {
|
||||
|
||||
public abstract class NumberRange<T> {
|
||||
public T min;
|
||||
public T max;
|
||||
|
||||
public abstract float Lerp(float t);
|
||||
public abstract float LerpUnclamped(float t);
|
||||
// returns 0 to 1
|
||||
public abstract float InverseLerp(float t);
|
||||
// returns values -1 to 1
|
||||
public abstract float InverseLerpSigned(float t);
|
||||
public abstract T Random();
|
||||
}
|
||||
|
||||
|
||||
[Serializable]
|
||||
public class FloatRange : NumberRange<float> {
|
||||
public FloatRange(float min, float max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
public override float Lerp(float t) => Mathf.Lerp(min, max, t);
|
||||
public override float LerpUnclamped(float t) => Mathf.LerpUnclamped(min, max, t);
|
||||
public override float InverseLerp(float t) => Mathf.InverseLerp(min, max, t);
|
||||
public override float InverseLerpSigned(float t) => Mathf.InverseLerp(min, max, t) * 2f - 1f;
|
||||
public override float Random() => UnityEngine.Random.Range(min, max);
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class IntRange : NumberRange<int> {
|
||||
public IntRange(int min, int max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
public override float Lerp(float t) => Mathf.Lerp(min, max, t);
|
||||
public override float LerpUnclamped(float t) => Mathf.LerpUnclamped(min, max, t);
|
||||
public override float InverseLerp(float t) => Mathf.InverseLerp(min, max, t);
|
||||
public override float InverseLerpSigned(float t) => Mathf.InverseLerp(min, max, t) * 2f - 1f;
|
||||
public override int Random() => UnityEngine.Random.Range(min, max);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user