Wwise Audio Integration
This commit is contained in:
parent
e7e305e6a0
commit
7d67d7ec2c
72 changed files with 4551 additions and 11819 deletions
|
|
@ -80,25 +80,25 @@ public class DatabaseInspector : EditorWindow
|
|||
}
|
||||
}
|
||||
|
||||
public void Inspect(object obj, bool inspectablesOnly = true)
|
||||
public void Inspect(object obj, object parent = null)
|
||||
{
|
||||
foreach (var field in obj.GetType().GetFields().OrderBy(f=>f.GetCustomAttribute<KeyAttribute>()?.IntKey ?? 0))
|
||||
{
|
||||
Inspect(obj, field, inspectablesOnly);
|
||||
Inspect(obj, field, parent);
|
||||
}
|
||||
}
|
||||
|
||||
public void Inspect(object obj, FieldInfo field, bool inspectablesOnly = true)
|
||||
public void Inspect(object obj, FieldInfo field, object parent = null)
|
||||
{
|
||||
var inspectable = field.GetCustomAttribute<InspectableAttribute>();
|
||||
if (inspectable == null && inspectablesOnly) return;
|
||||
if (inspectable == null) return;
|
||||
|
||||
var attributes = field.GetCustomAttributes(false);
|
||||
|
||||
var type = field.FieldType;
|
||||
var value = field.GetValue(obj);
|
||||
|
||||
field.SetValue(obj, InspectMember(field.Name.SplitCamelCase(), value, obj, type, false, attributes));
|
||||
field.SetValue(obj, InspectMember(field.Name.SplitCamelCase(), value, parent ?? obj, type, false, attributes));
|
||||
}
|
||||
|
||||
public object InspectMember(string label, object value, object parent, Type type, bool suppressContainer = false, params object[] attributes)
|
||||
|
|
@ -159,7 +159,7 @@ public class DatabaseInspector : EditorWindow
|
|||
else if (value == null) value = Activator.CreateInstance(type);
|
||||
}
|
||||
|
||||
if (value != null && (_foldouts.Contains(value) || suppressContainer)) Inspect(value);
|
||||
if (value != null && (_foldouts.Contains(value) || suppressContainer)) Inspect(value, parent);
|
||||
if(!suppressContainer)
|
||||
GUILayout.EndVertical();
|
||||
_listItemStyle = style;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.Mathematics;
|
||||
using UnityEditor;
|
||||
|
|
@ -110,7 +112,56 @@ public class ShapeInspector : BaseInspector<Shape>
|
|||
|
||||
public override Shape Inspect(string label, Shape value, object parent, DatabaseInspector inspectorWindow)
|
||||
{
|
||||
var hull = parent as HullData;
|
||||
if(value == null)
|
||||
{
|
||||
value = new Shape(1, 1);
|
||||
value[int2(0)] = true;
|
||||
}
|
||||
using (new VerticalScope())
|
||||
{
|
||||
using (new HorizontalScope())
|
||||
{
|
||||
GUILayout.Label(label, GUILayout.Width(width));
|
||||
GUILayout.Label("Size", GUILayout.ExpandWidth(false));
|
||||
GUILayout.Label("X", GUILayout.Width(labelWidth));
|
||||
value.Width = DelayedIntField(value.Width);
|
||||
GUILayout.Label("Y", GUILayout.Width(labelWidth));
|
||||
value.Height = DelayedIntField(value.Height);
|
||||
}
|
||||
|
||||
using (new HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("", GUILayout.Width(width));
|
||||
using (new VerticalScope())
|
||||
{
|
||||
for (var y = value.Height - 1; y >= 0; y--)
|
||||
{
|
||||
using (new HorizontalScope())
|
||||
{
|
||||
for (var x = 0; x < value.Width; x++)
|
||||
{
|
||||
var oldColor = GUI.backgroundColor;
|
||||
value[int2(x, y)] = Toggle(value[int2(x, y)], GUILayout.Width(toggleWidth));
|
||||
GUI.backgroundColor = oldColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShapeSchematicInspector : BaseInspector<Shape, InspectableSchematicShapeAttribute>
|
||||
{
|
||||
private Material _schematicMat = new Material(Shader.Find("Legacy Shaders/Particles/Additive"));
|
||||
|
||||
public override Shape Inspect(string label, Shape value, object parent, DatabaseInspector inspectorWindow, InspectableSchematicShapeAttribute attribute)
|
||||
{
|
||||
var item = parent as EquippableItemData;
|
||||
if(value == null)
|
||||
{
|
||||
value = new Shape(1, 1);
|
||||
|
|
@ -119,8 +170,8 @@ public class ShapeInspector : BaseInspector<Shape>
|
|||
using (new VerticalScope())
|
||||
{
|
||||
Texture2D schematic = null;
|
||||
if(hull != null && !string.IsNullOrEmpty(hull.Schematic))
|
||||
schematic = AssetDatabase.LoadAssetAtPath<Texture2D>(hull.Schematic);
|
||||
if(item != null && !string.IsNullOrEmpty(item.Schematic))
|
||||
schematic = AssetDatabase.LoadAssetAtPath<Texture2D>(item.Schematic);
|
||||
using (new HorizontalScope())
|
||||
{
|
||||
GUILayout.Label(label, GUILayout.Width(width));
|
||||
|
|
@ -158,7 +209,7 @@ public class ShapeInspector : BaseInspector<Shape>
|
|||
for (var x = 0; x < value.Width; x++)
|
||||
{
|
||||
var oldColor = GUI.backgroundColor;
|
||||
if(hull != null)
|
||||
if(item is HullData hull)
|
||||
{
|
||||
var hardpoint = hull.Hardpoints
|
||||
.FirstOrDefault(hp =>
|
||||
|
|
@ -180,4 +231,93 @@ public class ShapeInspector : BaseInspector<Shape>
|
|||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public class WwiseSoundBankInspector : BaseInspector<uint, InspectableSoundBankAttribute>
|
||||
{
|
||||
public override uint Inspect(string label, uint value, object parent, DatabaseInspector inspectorWindow, InspectableSoundBankAttribute attribute)
|
||||
{
|
||||
using (new HorizontalScope())
|
||||
{
|
||||
GUILayout.Label(label, GUILayout.Width(width));
|
||||
|
||||
var item = parent as EquippableItemData;
|
||||
if (item == null)
|
||||
{
|
||||
using (new HorizontalScope(GUI.skin.box))
|
||||
{
|
||||
GUILayout.Label("Unsupported Parent Type!");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<WwiseMetaSoundBank> soundBanks = ActionGameManager.SoundBanksInfo.SoundBanks;
|
||||
|
||||
if (item.Behaviors.Any(b => b is WeaponData))
|
||||
soundBanks = soundBanks.Where(s => s.GetEvent(WeaponAudioEvent.Fire) != null);
|
||||
|
||||
if (item.Behaviors.Any(b => b is ChargedWeaponData))
|
||||
soundBanks = soundBanks.Where(s => s.GetEvent(ChargedWeaponAudioEvent.Start) != null);
|
||||
|
||||
if (item.Behaviors.Any(b => b is ReactorData || b is ThrusterData || b is AetherDriveData))
|
||||
soundBanks = soundBanks.Where(s =>
|
||||
s.GetEvent(LoopingAudioEvent.Play) != null &&
|
||||
s.GetEvent(LoopingAudioEvent.Stop) != null &&
|
||||
s.GetParameter(SpecialAudioParameter.Intensity) != null);
|
||||
|
||||
var soundBanksArray = soundBanks.ToArray();
|
||||
|
||||
if (soundBanksArray.Length==0)
|
||||
{
|
||||
using (new HorizontalScope(GUI.skin.box))
|
||||
{
|
||||
GUILayout.Label("Sound bank not found!");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
var selected = Array.FindIndex(soundBanksArray, o=>o.Id==value);
|
||||
if (selected == -1) selected = 0;
|
||||
|
||||
value = soundBanksArray[Popup(selected, soundBanksArray.Select(p => p.ShortName).ToArray())].Id;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WwiseParameterBindingInspector : BaseInspector<uint, InspectableAudioParameterAttribute>
|
||||
{
|
||||
public override uint Inspect(string label, uint value, object parent, DatabaseInspector inspectorWindow, InspectableAudioParameterAttribute attribute)
|
||||
{
|
||||
using (new HorizontalScope())
|
||||
{
|
||||
GUILayout.Label(label, GUILayout.Width(width));
|
||||
|
||||
var item = parent as EquippableItemData;
|
||||
if (item == null)
|
||||
{
|
||||
using (new HorizontalScope(GUI.skin.box))
|
||||
{
|
||||
GUILayout.Label("Unsupported Parent Type!");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
var soundbank = ActionGameManager.SoundBanksInfo.SoundBanks.FirstOrDefault(s => s.Id == item.SoundBank);
|
||||
if (soundbank == null)
|
||||
{
|
||||
using (new HorizontalScope(GUI.skin.box))
|
||||
{
|
||||
GUILayout.Label("Sound bank not found!");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
var selected = Array.FindIndex(soundbank.GameParameters, o=>o.Id==value);
|
||||
if (selected == -1) selected = 0;
|
||||
|
||||
value = soundbank.GameParameters[Popup(selected, soundbank.GameParameters.Select(p => p.Name).ToArray())].Id;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ public static class CultClient {
|
|||
case LoginSuccessMessage loginSuccess:
|
||||
_token = loginSuccess.Session;
|
||||
break;
|
||||
case ErrorMessage error:
|
||||
case NetErrorMessage error:
|
||||
OnError?.Invoke(error.Error);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using Cinemachine;
|
|||
using Ink;
|
||||
using Ink.Runtime;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
|
|
@ -36,6 +37,21 @@ public class ActionGameManager : MonoBehaviour
|
|||
get => _gameDataDirectory ??= new DirectoryInfo(Application.dataPath).Parent.CreateSubdirectory("GameData");
|
||||
}
|
||||
|
||||
private static WwiseMetaSoundBanksInfo _soundBanksInfo;
|
||||
public static WwiseMetaSoundBanksInfo SoundBanksInfo => _soundBanksInfo ??=
|
||||
JsonConvert.DeserializeObject<WwiseMetadataFile>(File.ReadAllText(Path.Combine(GameDataDirectory.FullName, "SoundbanksInfo.json")))
|
||||
.SoundBanksInfo;
|
||||
|
||||
private static HashSet<uint> _loadedSoundBanks = new HashSet<uint>();
|
||||
public static void LoadSoundbank(uint soundBank)
|
||||
{
|
||||
if (!_loadedSoundBanks.Contains(soundBank))
|
||||
{
|
||||
AkSoundEngine.LoadBank(SoundBanksInfo.GetSoundBank(soundBank).ShortName, out _);
|
||||
_loadedSoundBanks.Add(soundBank);
|
||||
}
|
||||
}
|
||||
|
||||
private static CultCache _cultCache;
|
||||
|
||||
public static CultCache CultCache
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ public class EntityInstance : MonoBehaviour
|
|||
private (Transform transform, MeshRenderer meshRenderer) _currentPing;
|
||||
private float _pingBrightness;
|
||||
private Sensor _sensor;
|
||||
|
||||
private (Reactor reactor, GameObject sfxSource) _reactor;
|
||||
private Dictionary<Radiator, GameObject> _radiatorSfx = new Dictionary<Radiator, GameObject>();
|
||||
|
||||
private List<(GameObject source, EquippedItem item)> _audioSources = new List<(GameObject source, EquippedItem item)>();
|
||||
// private (Reactor reactor, GameObject sfxSource) _reactor;
|
||||
// private Dictionary<Radiator, GameObject> _radiatorSfx = new Dictionary<Radiator, GameObject>();
|
||||
|
||||
private static Dictionary<InstantWeaponData, InstantWeaponEffectManager> _instantWeaponManagers = new Dictionary<InstantWeaponData, InstantWeaponEffectManager>();
|
||||
private static Dictionary<ConstantWeaponData, ConstantWeaponEffectManager> _constantWeaponManagers = new Dictionary<ConstantWeaponData, ConstantWeaponEffectManager>();
|
||||
|
|
@ -231,36 +232,24 @@ public class EntityInstance : MonoBehaviour
|
|||
}
|
||||
|
||||
var hp = Entity.Hardpoints[item.Position.x, item.Position.y];
|
||||
if (hp != null && !string.IsNullOrEmpty(item.Data.SoundEffectTrigger))
|
||||
if (hp != null && item.Data.SoundBank != 0)
|
||||
{
|
||||
if(hp.Type == HardpointType.Radiator)
|
||||
var hardpointTransform = EquipmentHardpoints.FirstOrDefault(x => x.name == hp.Transform);
|
||||
if (hardpointTransform != null)
|
||||
{
|
||||
var radiatorHardpoint = RadiatorHardpoints.FirstOrDefault(x => x.name == hp.Transform);
|
||||
if (radiatorHardpoint)
|
||||
var hardpointGameObject = hardpointTransform.gameObject;
|
||||
_audioSources.Add((hardpointGameObject, item));
|
||||
AkSoundEngine.RegisterGameObj(hardpointGameObject);
|
||||
ActionGameManager.LoadSoundbank(item.Data.SoundBank);
|
||||
_subscriptions.Add(item.AudioEvents.Subscribe(e=>AkSoundEngine.PostEvent(e, hardpointGameObject)));
|
||||
_subscriptions.Add(item.AudioParameters.Subscribe(p=>AkSoundEngine.SetRTPCValue(p.id, p.v, hardpointGameObject)));
|
||||
var soundBank = ActionGameManager.SoundBanksInfo.GetSoundBank(item.Data.SoundBank);
|
||||
item.SoundBank = soundBank;
|
||||
if (soundBank.GetEvent(LoopingAudioEvent.Play) != null && soundBank.GetEvent(ChargedWeaponAudioEvent.Start) == null)
|
||||
{
|
||||
var rad = item.GetBehavior<Radiator>();
|
||||
if(rad != null && !string.IsNullOrEmpty(item.Data.SoundEffectTrigger))
|
||||
{
|
||||
AkSoundEngine.RegisterGameObj(radiatorHardpoint.gameObject);
|
||||
AkSoundEngine.PostEvent(item.Data.SoundEffectTrigger, radiatorHardpoint.gameObject);
|
||||
_radiatorSfx[rad] = radiatorHardpoint.gameObject;
|
||||
}
|
||||
AkSoundEngine.PostEvent(soundBank.GetEvent(LoopingAudioEvent.Play).Id, hardpointGameObject);
|
||||
}
|
||||
}
|
||||
// if(hp.Type == HardpointType.Reactor)
|
||||
// {
|
||||
// var reactorHardpoint = EquipmentHardpoints.FirstOrDefault(x => x.name == hp.Transform);
|
||||
// if (reactorHardpoint)
|
||||
// {
|
||||
// var reactor = item.GetBehavior<Reactor>();
|
||||
// if( reactor != null && !string.IsNullOrEmpty(item.Data.SoundEffectTrigger) )
|
||||
// {
|
||||
// AkSoundEngine.RegisterGameObj(reactorHardpoint.gameObject);
|
||||
// AkSoundEngine.PostEvent(item.Data.SoundEffectTrigger, reactorHardpoint.gameObject);
|
||||
// _reactor = (reactor, reactorHardpoint.gameObject);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
RadiatorMeshes = new Dictionary<Radiator, MeshRenderer>();
|
||||
|
|
@ -522,25 +511,6 @@ public class EntityInstance : MonoBehaviour
|
|||
foreach(var material in _fadeMaterials) material.SetFloat("_Fade", _fade);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var r in _radiatorSfx)
|
||||
{
|
||||
r.Value.SetActive(r.Key.Item.Online.Value);
|
||||
AkSoundEngine.SetObjectPosition(r.Value, r.Value.transform);
|
||||
AkSoundEngine.SetRTPCValue("performance_durability", r.Key.Item.DurabilityPerformance, r.Value);
|
||||
AkSoundEngine.SetRTPCValue("performance_thermal", r.Key.Item.ThermalPerformance, r.Value);
|
||||
AkSoundEngine.SetRTPCValue("performance_quality", r.Key.Item.EquippableItem.Quality, r.Value);
|
||||
}
|
||||
|
||||
// if (_reactor.reactor != null)
|
||||
// {
|
||||
// _reactor.sfxSource.SetActive(_reactor.reactor.Item.Online.Value);
|
||||
// AkSoundEngine.SetObjectPosition(_reactor.sfxSource, _reactor.sfxSource.transform);
|
||||
// AkSoundEngine.SetRTPCValue("reactor_load", _reactor.reactor.CurrentLoadRatio, _reactor.sfxSource);
|
||||
// AkSoundEngine.SetRTPCValue("performance_durability", _reactor.reactor.Item.DurabilityPerformance, _reactor.sfxSource);
|
||||
// AkSoundEngine.SetRTPCValue("performance_thermal", _reactor.reactor.Item.ThermalPerformance, _reactor.sfxSource);
|
||||
// AkSoundEngine.SetRTPCValue("performance_quality", _reactor.reactor.Item.EquippableItem.Quality, _reactor.sfxSource);
|
||||
// }
|
||||
|
||||
foreach (var x in RadiatorMeshes)
|
||||
{
|
||||
|
|
@ -560,16 +530,14 @@ public class EntityInstance : MonoBehaviour
|
|||
public virtual void OnDestroy()
|
||||
{
|
||||
Destroy(LocalSpace.gameObject);
|
||||
foreach (var r in _radiatorSfx)
|
||||
foreach (var (source, item) in _audioSources)
|
||||
{
|
||||
AkSoundEngine.PostEvent($"{r.Key.Item.Data.SoundEffectTrigger}_stop", r.Value);
|
||||
AkSoundEngine.UnregisterGameObj(r.Value);
|
||||
}
|
||||
|
||||
if (_reactor.reactor != null)
|
||||
{
|
||||
AkSoundEngine.PostEvent($"{_reactor.reactor.Item.Data.SoundEffectTrigger}_stop", _reactor.sfxSource);
|
||||
AkSoundEngine.UnregisterGameObj(_reactor.sfxSource);
|
||||
var soundBank = ActionGameManager.SoundBanksInfo.GetSoundBank(item.Data.SoundBank);
|
||||
if (soundBank.GetEvent(LoopingAudioEvent.Stop) != null)
|
||||
{
|
||||
AkSoundEngine.PostEvent(soundBank.GetEvent(LoopingAudioEvent.Stop).Id, source);
|
||||
}
|
||||
AkSoundEngine.UnregisterGameObj(source);
|
||||
}
|
||||
foreach(var x in _subscriptions)
|
||||
x.Dispose();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ public class ShipInstance : EntityInstance
|
|||
private class ThrusterInstance
|
||||
{
|
||||
public Thruster Thruster;
|
||||
public GameObject SfxSource;
|
||||
public ParticleSystem System;
|
||||
public float BaseEmission;
|
||||
public int MaxParticleCount;
|
||||
|
|
@ -53,7 +52,6 @@ public class ShipInstance : EntityInstance
|
|||
return new ThrusterInstance
|
||||
{
|
||||
Thruster = thruster,
|
||||
SfxSource = thrusterHardpoint?.gameObject,
|
||||
System = particles,
|
||||
BaseEmission = particles.emission.rateOverTimeMultiplier,
|
||||
MaxParticleCount = 0
|
||||
|
|
@ -99,31 +97,8 @@ public class ShipInstance : EntityInstance
|
|||
var data = Entity.ItemManager.GetData(item);
|
||||
thrusterInstance.MaxParticleCount = thrusterInstance.System.particleCount;
|
||||
emissionModule.rateOverTimeMultiplier = thrusterInstance.BaseEmission * thrusterInstance.Thruster.Axis * (item.Durability / data.Durability);
|
||||
// if (thrusterInstance.SfxSource)
|
||||
// {
|
||||
// var throttle = 0f;
|
||||
// if (thrusterInstance.System.particleCount > 0)
|
||||
// throttle = (float) thrusterInstance.System.particleCount / thrusterInstance.MaxParticleCount;
|
||||
// AkSoundEngine.SetObjectPosition(thrusterInstance.SfxSource, thrusterInstance.SfxSource.transform);
|
||||
// AkSoundEngine.SetRTPCValue("thruster_throttle", throttle, thrusterInstance.SfxSource);
|
||||
// AkSoundEngine.SetRTPCValue("performance_durability", thrusterInstance.Thruster.Item.DurabilityPerformance, thrusterInstance.SfxSource);
|
||||
// AkSoundEngine.SetRTPCValue("performance_thermal", thrusterInstance.Thruster.Item.ThermalPerformance, thrusterInstance.SfxSource);
|
||||
// AkSoundEngine.SetRTPCValue("performance_quality", thrusterInstance.Thruster.Item.EquippableItem.Quality, thrusterInstance.SfxSource);
|
||||
// }
|
||||
}
|
||||
|
||||
transform.rotation = Ship.Rotation;
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
foreach (var thrusterInstance in _thrusters)
|
||||
{
|
||||
if (thrusterInstance.SfxSource)
|
||||
{
|
||||
AkSoundEngine.PostEvent(thrusterInstance.Thruster.Item.Data.SoundEffectTrigger + "_stop", thrusterInstance.SfxSource);
|
||||
AkSoundEngine.UnregisterGameObj(thrusterInstance.SfxSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,12 @@ public class AetherDriveData : BehaviorData
|
|||
|
||||
[Inspectable, JsonProperty("passiveCoupling"), Key(10), RuntimeInspectable]
|
||||
public PerformanceStat PassiveCoupling;
|
||||
|
||||
[InspectableAudioParameter, JsonProperty("rpmAudio"), Key(11), RuntimeInspectable]
|
||||
public uint RpmAudioParameter;
|
||||
|
||||
[InspectableAudioParameter, JsonProperty("torqueAudio"), Key(12), RuntimeInspectable]
|
||||
public uint TorqueRatioAudioParameter;
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
|
|
@ -119,6 +125,10 @@ public class AetherDrive : Behavior
|
|||
var torqueRatio = actualRpmDelta / potentialRpmDelta;
|
||||
var draw = torqueRatio * Evaluate(_data.EnergyDraw) / 3;
|
||||
|
||||
Item.SetAudioParameter(SpecialAudioParameter.Intensity, max(max(_axis.x, _axis.y), _axis.z));
|
||||
Item.SetAudioParameter(_data.RpmAudioParameter, (Rpm.x + Rpm.y + Rpm.z) / 3 / MaximumRpm);
|
||||
Item.SetAudioParameter(_data.TorqueRatioAudioParameter, max(max(torqueRatio.x, torqueRatio.y), torqueRatio.z));
|
||||
|
||||
if (Entity.TryConsumeEnergy((draw.x + draw.y + draw.z)*dt))
|
||||
{
|
||||
Rpm += actualRpmDelta;
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ public class ChargedWeapon : InstantWeapon
|
|||
if (_charging)
|
||||
{
|
||||
_charge += dt / ChargeTime;
|
||||
Item.SetAudioParameter(SpecialAudioParameter.ChargeLevel, saturate(_charge));
|
||||
if (!_charged)
|
||||
{
|
||||
AddHeat(ChargeHeat * (dt / ChargeTime));
|
||||
|
|
@ -143,6 +144,7 @@ public class ChargedWeapon : InstantWeapon
|
|||
_coolingDown = true;
|
||||
_charge = 0;
|
||||
OnFailed?.Invoke();
|
||||
Item.FireAudioEvent(ChargedWeaponAudioEvent.Fail);
|
||||
CauseDamage(_data.FailureDamage);
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +156,7 @@ public class ChargedWeapon : InstantWeapon
|
|||
if(!_charging && !_coolingDown)
|
||||
{
|
||||
OnStartCharging?.Invoke();
|
||||
Item.FireAudioEvent(ChargedWeaponAudioEvent.Start);
|
||||
_charging = true;
|
||||
_charged = false;
|
||||
}
|
||||
|
|
@ -169,6 +172,7 @@ public class ChargedWeapon : InstantWeapon
|
|||
_charge = 0;
|
||||
}
|
||||
OnStopCharging?.Invoke();
|
||||
Item.FireAudioEvent(ChargedWeaponAudioEvent.Stop);
|
||||
_charging = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,8 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
|||
OnCooldownComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var firedThisFrame = false;
|
||||
_burstTimer += dt;
|
||||
while (_burstRemaining > 0 && _burstTimer > 0)
|
||||
{
|
||||
|
|
@ -180,6 +181,11 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
|||
_burstRemaining--;
|
||||
_burstTimer -= _burstInterval;
|
||||
OnFire?.Invoke();
|
||||
if(!firedThisFrame)
|
||||
{
|
||||
Item.FireAudioEvent(WeaponAudioEvent.Fire);
|
||||
firedThisFrame = true;
|
||||
}
|
||||
CauseWearDamage(1);
|
||||
AddHeat(Heat);
|
||||
Entity.VisibilitySources[this] = Visibility;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ public class Reactor : Behavior, IOrderedBehavior, IDisposable
|
|||
CurrentLoadRatio = 1;
|
||||
}
|
||||
|
||||
Item.SetAudioParameter(SpecialAudioParameter.Intensity, max(.25f, 1 - 1 / CurrentLoadRatio));
|
||||
|
||||
AddHeat(heat);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ public class Thruster : Behavior, IAnalogBehavior
|
|||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
Item.SetAudioParameter(SpecialAudioParameter.Intensity, _input);
|
||||
if(_input > .01f && Entity.TryConsumeEnergy(_input * Evaluate(_data.EnergyUsage)))
|
||||
{
|
||||
Thrust = Evaluate(_data.Thrust);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ public class InspectableTextAssetAttribute : PreferredInspectorAttribute { }
|
|||
public class InspectableTemperatureAttribute : PreferredInspectorAttribute { }
|
||||
public class InspectableAnimationCurveAttribute : PreferredInspectorAttribute { }
|
||||
public class InspectableColorAttribute : PreferredInspectorAttribute { }
|
||||
public class InspectableSoundBankAttribute : PreferredInspectorAttribute { }
|
||||
public class InspectableAudioParameterAttribute : PreferredInspectorAttribute { }
|
||||
public class InspectableSchematicShapeAttribute : PreferredInspectorAttribute { }
|
||||
|
||||
public class InspectableEnumValuesAttribute : PreferredInspectorAttribute
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,7 @@ public class EquippedItem
|
|||
|
||||
private bool _thermalOnline;
|
||||
private bool _durabilityOnline;
|
||||
public WwiseMetaSoundBank SoundBank;
|
||||
|
||||
public Behavior[] Behaviors { get; }
|
||||
public Dictionary<int, BehaviorGroup> BehaviorGroups { get; }
|
||||
|
|
@ -1053,6 +1054,10 @@ public class EquippedItem
|
|||
public ReadOnlyReactiveProperty<bool> Active { get; }
|
||||
public ItemManager ItemManager { get; }
|
||||
|
||||
public Subject<uint> AudioEvents { get; } = new Subject<uint>();
|
||||
public Subject<(uint id, float v)> AudioParameters { get; } = new Subject<(uint id, float v)>();
|
||||
public Dictionary<uint, float> AudioParameterValues { get; } = new Dictionary<uint, float>();
|
||||
|
||||
public float Temperature
|
||||
{
|
||||
get
|
||||
|
|
@ -1062,8 +1067,77 @@ public class EquippedItem
|
|||
return sum/InsetShape.Coordinates.Length;
|
||||
}
|
||||
}
|
||||
|
||||
//private bool _online;
|
||||
|
||||
public void FireAudioEvent(uint eventId, bool skipVerify = false)
|
||||
{
|
||||
if(SoundBank != null && (skipVerify || SoundBank.IncludedEvents.Any(o => o.Id == eventId)))
|
||||
AudioEvents.OnNext(eventId);
|
||||
}
|
||||
|
||||
public void FireAudioEvent(WwiseSoundBinding soundBinding)
|
||||
{
|
||||
FireAudioEvent(soundBinding.PlayEvent);
|
||||
}
|
||||
|
||||
public void PlaySound(WwiseLoopingSoundBinding soundBinding)
|
||||
{
|
||||
FireAudioEvent(soundBinding.PlayEvent);
|
||||
}
|
||||
|
||||
public void StopSound(WwiseLoopingSoundBinding soundBinding)
|
||||
{
|
||||
FireAudioEvent(soundBinding.StopEvent);
|
||||
}
|
||||
|
||||
public void FireAudioEvent(WeaponAudioEvent weaponAudioEvent)
|
||||
{
|
||||
if (SoundBank == null) return;
|
||||
var eventObject = SoundBank.GetEvent(weaponAudioEvent);
|
||||
if (eventObject == null)
|
||||
{
|
||||
ItemManager.Log($"Attempted to trigger {Enum.GetName(typeof(WeaponAudioEvent), weaponAudioEvent)} weapon audio event, but the soundbank doesn't support it!");
|
||||
return;
|
||||
}
|
||||
FireAudioEvent(eventObject.Id, true);
|
||||
}
|
||||
|
||||
public void FireAudioEvent(ChargedWeaponAudioEvent weaponAudioEvent)
|
||||
{
|
||||
if (SoundBank == null) return;
|
||||
var eventObject = SoundBank.GetEvent(weaponAudioEvent);
|
||||
if (eventObject == null)
|
||||
{
|
||||
ItemManager.Log($"Attempted to trigger {Enum.GetName(typeof(ChargedWeaponAudioEvent), weaponAudioEvent)} weapon audio event, but the soundbank doesn't support it!");
|
||||
return;
|
||||
}
|
||||
FireAudioEvent(eventObject.Id, true);
|
||||
}
|
||||
|
||||
public void SetAudioParameter(uint id, float v, bool skipVerify = false)
|
||||
{
|
||||
if (SoundBank != null && (skipVerify || SoundBank.GameParameters.Any(o => o.Id == id)))
|
||||
{
|
||||
AudioParameterValues[id] = v;
|
||||
AudioParameters.OnNext((id, v));
|
||||
}
|
||||
}
|
||||
|
||||
// public void SetAudioParameter(WwiseParameterBinding binding)
|
||||
// {
|
||||
// FireAudioEvent(binding.Parameter);
|
||||
// }
|
||||
|
||||
public void SetAudioParameter(SpecialAudioParameter p, float v)
|
||||
{
|
||||
if (SoundBank == null) return;
|
||||
var metaObject = SoundBank.GetParameter(p);
|
||||
if (metaObject == null)
|
||||
{
|
||||
ItemManager.Log($"Attempted to set {Enum.GetName(typeof(ChargedWeaponAudioEvent), p)} audio parameter, but the soundbank doesn't support it!");
|
||||
return;
|
||||
}
|
||||
SetAudioParameter(metaObject.Id, v, true);
|
||||
}
|
||||
|
||||
public EquippedItem(ItemManager itemManager, EquippableItem item, int2 position, Entity entity)
|
||||
{
|
||||
|
|
@ -1152,6 +1226,11 @@ public class EquippedItem
|
|||
|
||||
public void Update(float delta)
|
||||
{
|
||||
foreach (var audioStat in Data.AudioStats)
|
||||
{
|
||||
SetAudioParameter(audioStat.Parameter, Evaluate(audioStat.Stat));
|
||||
}
|
||||
|
||||
if (Active.Value)
|
||||
{
|
||||
foreach (var group in BehaviorGroups.Values)
|
||||
|
|
|
|||
|
|
@ -170,4 +170,32 @@ public enum TemperatureUnit
|
|||
Kelvin,
|
||||
Celsius,
|
||||
Fahrenheit
|
||||
}
|
||||
|
||||
public enum WeaponAudioEvent
|
||||
{
|
||||
Fire,
|
||||
Hit,
|
||||
Miss
|
||||
}
|
||||
|
||||
public enum ChargedWeaponAudioEvent
|
||||
{
|
||||
Start,
|
||||
Stop,
|
||||
Fail
|
||||
}
|
||||
|
||||
public enum SpecialAudioParameter
|
||||
{
|
||||
ShipVelocity,
|
||||
ChargeLevel,
|
||||
TargetLock,
|
||||
Intensity,
|
||||
}
|
||||
|
||||
public enum LoopingAudioEvent
|
||||
{
|
||||
Play,
|
||||
Stop
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ public abstract class ItemData : DatabaseEntry, INamedEntry
|
|||
[Inspectable, JsonProperty("mass"), Key(4)]
|
||||
public float Mass;
|
||||
|
||||
[Inspectable, JsonProperty("shape"), Key(5)]
|
||||
[InspectableSchematicShape, JsonProperty("shape"), Key(5)]
|
||||
public Shape Shape;
|
||||
|
||||
// Heat needed to change temperature of 1 gram by 1 degree
|
||||
|
|
@ -347,11 +347,17 @@ public abstract class EquippableItemData : CraftedItemData
|
|||
[Inspectable, JsonProperty("resilience"), Key(18)]
|
||||
public float ThermalResilience = 1;
|
||||
|
||||
[Inspectable, JsonProperty("sfx"), Key(19)]
|
||||
public string SoundEffectTrigger;
|
||||
// [Inspectable, JsonProperty("sfx"), Key(19)]
|
||||
// public string SoundEffectTrigger;
|
||||
|
||||
[InspectableTexture, JsonProperty("actionIcon"), Key(20)]
|
||||
public string ActionBarIcon;
|
||||
|
||||
[InspectableSoundBank, JsonProperty("soundBank"), Key(21)]
|
||||
public uint SoundBank;
|
||||
|
||||
[Inspectable, JsonProperty("audioStats"), Key(22)]
|
||||
public List<AudioStat> AudioStats = new List<AudioStat>();
|
||||
|
||||
[IgnoreMember]
|
||||
public abstract HardpointType HardpointType { get; }
|
||||
|
|
@ -391,10 +397,20 @@ public abstract class EquippableItemData : CraftedItemData
|
|||
}
|
||||
}
|
||||
|
||||
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class AudioStat
|
||||
{
|
||||
[InspectableAudioParameter, JsonProperty("parameter"), Key(0)]
|
||||
public uint Parameter;
|
||||
|
||||
[Inspectable, JsonProperty("stat"), Key(1)]
|
||||
public PerformanceStat Stat = new PerformanceStat();
|
||||
}
|
||||
|
||||
[RethinkTable("Items"), Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class GearData : EquippableItemData
|
||||
{
|
||||
[Inspectable, JsonProperty("hardpointType"), Key(21)]
|
||||
[Inspectable, JsonProperty("hardpointType"), Key(23)]
|
||||
public HardpointType Hardpoint;
|
||||
|
||||
[IgnoreMember] public override HardpointType HardpointType => Hardpoint;
|
||||
|
|
@ -403,56 +419,57 @@ public class GearData : EquippableItemData
|
|||
[RethinkTable("Items"), Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class CargoBayData : EquippableItemData
|
||||
{
|
||||
[Inspectable, JsonProperty("interiorShape"), Key(21)]
|
||||
[Inspectable, JsonProperty("interiorShape"), Key(24)]
|
||||
public Shape InteriorShape;
|
||||
|
||||
[IgnoreMember] public override HardpointType HardpointType => HardpointType.Tool;
|
||||
}
|
||||
|
||||
[RethinkTable("Items"), Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class DockingBayData : CargoBayData
|
||||
{
|
||||
[Inspectable, JsonProperty("maxSize"), Key(22)]
|
||||
[Inspectable, JsonProperty("maxSize"), Key(25)]
|
||||
public int2 MaxSize;
|
||||
}
|
||||
|
||||
[RethinkTable("Items"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class WeaponItemData : GearData
|
||||
{
|
||||
[Inspectable, JsonProperty("range"), Key(22)]
|
||||
[Inspectable, JsonProperty("range"), Key(24)]
|
||||
public WeaponRange WeaponRange;
|
||||
|
||||
[Inspectable, JsonProperty("caliber"), Key(23)]
|
||||
[Inspectable, JsonProperty("caliber"), Key(25)]
|
||||
public WeaponCaliber WeaponCaliber;
|
||||
|
||||
[Inspectable, JsonProperty("weaponType"), Key(24)]
|
||||
[Inspectable, JsonProperty("weaponType"), Key(26)]
|
||||
public WeaponType WeaponType;
|
||||
|
||||
[Inspectable, JsonProperty("fireTypes"), Key(25)]
|
||||
[Inspectable, JsonProperty("fireTypes"), Key(27)]
|
||||
public WeaponFireType WeaponFireTypes;
|
||||
|
||||
[Inspectable, JsonProperty("modifiers"), Key(26)]
|
||||
[Inspectable, JsonProperty("modifiers"), Key(28)]
|
||||
public WeaponModifiers WeaponModifiers;
|
||||
}
|
||||
|
||||
[RethinkTable("Items"), Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class HullData : EquippableItemData
|
||||
{
|
||||
[Inspectable, JsonProperty("hardpoints"), Key(21)]
|
||||
[Inspectable, JsonProperty("hardpoints"), Key(23)]
|
||||
public List<HardpointData> Hardpoints = new List<HardpointData>();
|
||||
|
||||
[InspectablePrefab, JsonProperty("prefab"), Key(22)]
|
||||
[InspectablePrefab, JsonProperty("prefab"), Key(24)]
|
||||
public string Prefab;
|
||||
|
||||
[Inspectable, JsonProperty("hullType"), Key(23)]
|
||||
[Inspectable, JsonProperty("hullType"), Key(25)]
|
||||
public HullType HullType;
|
||||
|
||||
[Inspectable, JsonProperty("gridOffset"), Key(24)]
|
||||
[Inspectable, JsonProperty("gridOffset"), Key(26)]
|
||||
public float GridOffset;
|
||||
|
||||
[Inspectable, JsonProperty("armor"), Key(25)]
|
||||
[Inspectable, JsonProperty("armor"), Key(27)]
|
||||
public float Armor;
|
||||
|
||||
[Inspectable, JsonProperty("drag"), Key(26)]
|
||||
[Inspectable, JsonProperty("drag"), Key(28)]
|
||||
public float Drag;
|
||||
|
||||
[IgnoreMember]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Unity.Mathematics;
|
|||
Union(1, typeof(LoginMessage)),
|
||||
Union(2, typeof(RegisterMessage)),
|
||||
Union(3, typeof(VerifyMessage)),
|
||||
Union(4, typeof(ErrorMessage)),
|
||||
Union(4, typeof(NetErrorMessage)),
|
||||
Union(5, typeof(LoginSuccessMessage)),
|
||||
Union(6, typeof(ChatMessage)),
|
||||
Union(7, typeof(ChatBroadcastMessage)),
|
||||
|
|
@ -53,7 +53,7 @@ public class VerifyMessage : Message
|
|||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class ErrorMessage : Message
|
||||
public class NetErrorMessage : Message
|
||||
{
|
||||
[Key(0)] public string Error;
|
||||
}
|
||||
|
|
|
|||
85
Assets/Scripts/ServerShared/WwiseMetadata.cs
Normal file
85
Assets/Scripts/ServerShared/WwiseMetadata.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetadataFile
|
||||
{
|
||||
public WwiseMetaSoundBanksInfo SoundBanksInfo;
|
||||
}
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetaSoundBanksInfo
|
||||
{
|
||||
public WwiseMetaSoundBank[] SoundBanks;
|
||||
|
||||
private Dictionary<uint, WwiseMetaSoundBank> _soundBanks;
|
||||
|
||||
public WwiseMetaSoundBank GetSoundBank(uint id)
|
||||
{
|
||||
if (_soundBanks == null) _soundBanks = SoundBanks.ToDictionary(sb => sb.Id);
|
||||
return _soundBanks[id];
|
||||
}
|
||||
}
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetaSoundBank
|
||||
{
|
||||
public uint Id;
|
||||
public string ShortName;
|
||||
public WwiseMetaObject[] IncludedEvents;
|
||||
public WwiseMetaObject[] GameParameters;
|
||||
|
||||
public WwiseMetaObject GetEvent(WeaponAudioEvent weaponAudioEvent)
|
||||
{
|
||||
return weaponAudioEvent switch
|
||||
{
|
||||
WeaponAudioEvent.Fire => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_fire")),
|
||||
WeaponAudioEvent.Hit => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_hit")),
|
||||
WeaponAudioEvent.Miss => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_miss")),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(weaponAudioEvent), weaponAudioEvent, null)
|
||||
};
|
||||
}
|
||||
|
||||
public WwiseMetaObject GetEvent(ChargedWeaponAudioEvent chargedWeaponAudioEvent)
|
||||
{
|
||||
return chargedWeaponAudioEvent switch
|
||||
{
|
||||
ChargedWeaponAudioEvent.Start => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_charge_play")),
|
||||
ChargedWeaponAudioEvent.Stop => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_charge_stop")),
|
||||
ChargedWeaponAudioEvent.Fail => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_fail")),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(chargedWeaponAudioEvent), chargedWeaponAudioEvent, null)
|
||||
};
|
||||
}
|
||||
|
||||
public WwiseMetaObject GetEvent(LoopingAudioEvent loopingAudioEvent)
|
||||
{
|
||||
return loopingAudioEvent switch
|
||||
{
|
||||
LoopingAudioEvent.Play => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_play")),
|
||||
LoopingAudioEvent.Stop => IncludedEvents?.FirstOrDefault(e => e.Name.EndsWith("_stop")),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(loopingAudioEvent), loopingAudioEvent, null)
|
||||
};
|
||||
}
|
||||
|
||||
public WwiseMetaObject GetParameter(SpecialAudioParameter audioParameter)
|
||||
{
|
||||
return audioParameter switch
|
||||
{
|
||||
SpecialAudioParameter.ShipVelocity => GameParameters?.FirstOrDefault(p => p.Name == "ship_velocity"),
|
||||
SpecialAudioParameter.ChargeLevel => GameParameters?.FirstOrDefault(p => p.Name == "charge_level"),
|
||||
SpecialAudioParameter.TargetLock => GameParameters?.FirstOrDefault(p => p.Name == "target_locking"),
|
||||
SpecialAudioParameter.Intensity => GameParameters?.FirstOrDefault(p => p.Name == "sfx_shared_intensity"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(audioParameter), audioParameter, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetaObject
|
||||
{
|
||||
public uint Id;
|
||||
public string Name;
|
||||
public string ObjectPath;
|
||||
}
|
||||
22
Assets/Scripts/ServerShared/WwiseSoundBinding.cs
Normal file
22
Assets/Scripts/ServerShared/WwiseSoundBinding.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class WwiseSoundBinding
|
||||
{
|
||||
[Key(0), JsonProperty("playEvent")]
|
||||
public uint PlayEvent;
|
||||
}
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class WwiseLoopingSoundBinding : WwiseSoundBinding
|
||||
{
|
||||
[Key(1), JsonProperty("stopEvent")]
|
||||
public uint StopEvent;
|
||||
}
|
||||
|
||||
// public class WwiseParameterBinding
|
||||
// {
|
||||
// [Key(0), JsonProperty("parameter")]
|
||||
// public uint Parameter;
|
||||
// }
|
||||
3
Assets/Scripts/ServerShared/WwiseSoundBinding.cs.meta
Normal file
3
Assets/Scripts/ServerShared/WwiseSoundBinding.cs.meta
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2e128c1cb68f481c9a846344e8283014
|
||||
timeCreated: 1619729875
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetadataFile
|
||||
{
|
||||
public WwiseMetaSoundBanksInfo SoundBanksInfo;
|
||||
}
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetaSoundBanksInfo
|
||||
{
|
||||
public WwiseMetaSoundBank[] SoundBanks;
|
||||
}
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetaSoundBank
|
||||
{
|
||||
public int Id;
|
||||
public string ShortName;
|
||||
public WwiseMetaObject[] IncludedEvents;
|
||||
public WwiseMetaObject[] GameParameters;
|
||||
}
|
||||
|
||||
[JsonObject]
|
||||
public class WwiseMetaObject
|
||||
{
|
||||
public int Id;
|
||||
public string Name;
|
||||
public string ObjectPath;
|
||||
}
|
||||
Loading…
Reference in a new issue