first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
using System.Collections;
using System.IO;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxClient.Unity.Helper;
public static class AssetBundleLoader
{
private static readonly string assetRootFolder = NitroxUser.AssetBundlePath;
private static bool loadedSharedAssets;
private static IEnumerator LoadAssetBundle(NitroxAssetBundle nitroxAssetBundle)
{
if (IsBundleLoaded(nitroxAssetBundle))
{
yield break;
}
if (!loadedSharedAssets)
{
loadedSharedAssets = true;
yield return LoadAllAssets(NitroxAssetBundle.SHARED_ASSETS);
}
AssetBundleCreateRequest assetRequest;
using (Stream stream = File.Open(Path.Combine(assetRootFolder, nitroxAssetBundle.BundleName), FileMode.Open, FileAccess.Read, FileShare.Read))
{
assetRequest = AssetBundle.LoadFromStreamAsync(stream);
if (assetRequest == null)
{
Log.Error($"Failed to load AssetBundle: {nitroxAssetBundle.BundleName}");
yield break;
}
yield return assetRequest;
}
nitroxAssetBundle.AssetBundle = assetRequest.assetBundle;
}
public static IEnumerator LoadAllAssets(NitroxAssetBundle nitroxAssetBundle)
{
yield return LoadAssetBundle(nitroxAssetBundle);
AssetBundleRequest loadRequest = nitroxAssetBundle.AssetBundle.LoadAllAssetsAsync();
yield return loadRequest;
if (loadRequest.allAssets == null || loadRequest.allAssets.Length == 0)
{
Log.Error($"Failed to load AssetBundle: {nitroxAssetBundle.BundleName}. It contained no assets");
yield break;
}
nitroxAssetBundle.LoadedAssets = loadRequest.allAssets;
}
public static IEnumerator LoadUIAsset(NitroxAssetBundle nitroxAssetBundle, bool hideUI)
{
yield return LoadAssetBundle(nitroxAssetBundle);
AssetBundleRequest fetchAssetRequest = nitroxAssetBundle.AssetBundle.LoadAssetAsync<GameObject>(nitroxAssetBundle.BundleName);
yield return fetchAssetRequest;
GameObject asset = UnityEngine.Object.Instantiate(fetchAssetRequest.asset, uGUI.main.screenCanvas.transform, false) as GameObject;
if (!asset)
{
Log.Error($"Instantiated assetBundle ({nitroxAssetBundle.BundleName}) but GameObject is null.");
yield break;
}
if (hideUI && asset.TryGetComponent(out CanvasGroup canvasGroup))
{
canvasGroup.alpha = 0;
}
nitroxAssetBundle.LoadedAssets = new UnityEngine.Object[] { asset };
}
public static bool IsBundleLoaded(NitroxAssetBundle nitroxAssetBundle)
{
return nitroxAssetBundle.LoadedAssets != null && nitroxAssetBundle.LoadedAssets.Length > 0;
}
// ReSharper disable class StringLiteralTypo, InconsistentNaming
public class NitroxAssetBundle
{
public string BundleName { get; }
public AssetBundle AssetBundle { get; set; }
public UnityEngine.Object[] LoadedAssets { get; set; }
private NitroxAssetBundle(string bundleName)
{
BundleName = bundleName;
}
public static readonly NitroxAssetBundle SHARED_ASSETS = new("sharedassets");
public static readonly NitroxAssetBundle PLAYER_LIST_TAB = new("playerlisttab");
public static readonly NitroxAssetBundle CHAT_LOG = new("chatlog");
public static readonly NitroxAssetBundle CHAT_KEY_HINT = new("chatkeyhint");
public static readonly NitroxAssetBundle DISCORD_JOIN_REQUEST = new("discordjoinrequest");
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections;
namespace NitroxClient.Unity.Helper;
public static class CoroutineHelper
{
public static IEnumerator OnYieldError(this IEnumerator enumerator, Action<Exception> exceptionCallback)
{
return enumerator.OnYieldError<Exception>(exceptionCallback);
}
public static IEnumerator OnYieldError<T>(this IEnumerator enumerator, Action<T> exceptionCallback = null) where T : Exception
{
if (enumerator == null)
{
yield break;
}
while (true)
{
try
{
if (!enumerator.MoveNext())
{
yield break;
}
}
catch (T exception)
{
exceptionCallback?.Invoke(exception);
yield break;
}
yield return enumerator.Current;
}
}
}

View File

@@ -0,0 +1,70 @@
using NitroxClient.MonoBehaviours;
using UnityEngine;
namespace NitroxClient.Unity.Helper
{
public static class DebugUtils
{
public static void PrintHierarchy(GameObject gameObject, bool startAtRoot = false, int parentsUpwards = 1, bool listComponents = false, bool travelDown = true)
{
GameObject startHierarchy = gameObject;
if (startAtRoot)
{
GameObject rootObject = gameObject.transform.root.gameObject;
if (rootObject != null)
{
startHierarchy = rootObject;
}
}
else
{
GameObject parentObject = gameObject;
int i = 0;
while (i < parentsUpwards)
{
i++;
if (parentObject.transform.parent != null)
{
parentObject = parentObject.transform.parent.gameObject;
}
else
{
i = parentsUpwards;
}
}
}
TravelDown(startHierarchy, listComponents, "", travelDown);
}
private static void TravelDown(GameObject gameObject, bool listComponents = false, string linePrefix = "", bool travelDown = true)
{
NitroxEntity entity = gameObject.GetComponent<NitroxEntity>();
string guid = (entity) ? entity.Id.ToString() : "None";
Log.Debug($"{linePrefix}+GameObject GUID={guid} NAME={gameObject.name} POSITION={gameObject.transform.position}");
if (listComponents)
{
ListComponents(gameObject, linePrefix);
}
if (!travelDown)
{
return;
}
foreach (Transform child in gameObject.transform)
{
TravelDown(child.gameObject, listComponents, $"{linePrefix}| ");
}
}
private static void ListComponents(GameObject gameObject, string linePrefix = "")
{
Component[] allComponents = gameObject.GetComponents<Component>();
foreach (Component c in allComponents)
{
Log.Debug($"{linePrefix}=Component NAME={c.GetType().Name}");
}
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxClient.Unity.Helper
{
public static class GUISkinUtils
{
private static Dictionary<string, GUISkin> guiSkins = new Dictionary<string, GUISkin>();
public static GUISkin CreateDerived(GUISkin baseSkin = null, string name = null)
{
if (baseSkin == null)
{
GUISkin prevSkin = GUI.skin;
GUI.skin = null;
baseSkin = GUI.skin;
GUI.skin = prevSkin;
}
GUISkin copy = ScriptableObject.CreateInstance<GUISkin>();
copy.name = name ?? Guid.NewGuid().ToString();
copy.box = new GUIStyle(baseSkin.box);
copy.button = new GUIStyle(baseSkin.button);
copy.label = new GUIStyle(baseSkin.label);
copy.scrollView = new GUIStyle(baseSkin.scrollView);
copy.textArea = new GUIStyle(baseSkin.textArea);
copy.textField = new GUIStyle(baseSkin.textField);
copy.toggle = new GUIStyle(baseSkin.toggle);
copy.window = new GUIStyle(baseSkin.window);
copy.horizontalScrollbar = new GUIStyle(baseSkin.horizontalScrollbar);
copy.horizontalScrollbarLeftButton = new GUIStyle(baseSkin.horizontalScrollbarLeftButton);
copy.horizontalScrollbarRightButton = new GUIStyle(baseSkin.horizontalScrollbarRightButton);
copy.horizontalScrollbarThumb = new GUIStyle(baseSkin.horizontalScrollbarThumb);
copy.horizontalSlider = new GUIStyle(baseSkin.horizontalSlider);
copy.horizontalSliderThumb = new GUIStyle(baseSkin.horizontalSliderThumb);
copy.verticalScrollbar = new GUIStyle(baseSkin.verticalScrollbar);
copy.verticalScrollbarDownButton = new GUIStyle(baseSkin.verticalScrollbarDownButton);
copy.verticalScrollbarThumb = new GUIStyle(baseSkin.verticalScrollbarThumb);
copy.verticalScrollbarUpButton = new GUIStyle(baseSkin.verticalScrollbarUpButton);
copy.verticalSlider = new GUIStyle(baseSkin.verticalSlider);
copy.verticalSliderThumb = new GUIStyle(baseSkin.verticalSliderThumb);
copy.customStyles = baseSkin.customStyles.Select(s => new GUIStyle(s)).ToArray();
copy.hideFlags = baseSkin.hideFlags;
copy.settings.cursorColor = baseSkin.settings.cursorColor;
copy.settings.cursorFlashSpeed = baseSkin.settings.cursorFlashSpeed;
copy.settings.doubleClickSelectsWord = baseSkin.settings.doubleClickSelectsWord;
copy.settings.tripleClickSelectsLine = baseSkin.settings.tripleClickSelectsLine;
copy.settings.selectionColor = baseSkin.settings.selectionColor;
// TODO: Create identical copy of font.
copy.font = baseSkin.font; // Gives a giberish font: copy.font = UnityEngine.Object.Instantiate(baseSkin.font);
return copy;
}
public static GUISkin RegisterDerived(string name, GUISkin baseSkin = null)
{
Validate.NotNull(name);
Validate.IsFalse(guiSkins.ContainsKey(name), $"Name of new GUISkin already exists.");
GUISkin newSkin = CreateDerived(baseSkin, name);
guiSkins.Add(name, newSkin);
return newSkin;
}
/// <summary>
/// Creates a new default Unity skin if there are no existing skins with the given <paramref name="name"/>.
/// </summary>
/// <param name="name">Name for the new skin.</param>
/// <param name="skinInitializer">Optional skin initializer.</param>
/// <param name="baseSkin">Optional base skin to copy from.</param>
/// <returns>New or cached skin.</returns>
public static GUISkin RegisterDerivedOnce(string name, Action<GUISkin> skinInitializer = null, GUISkin baseSkin = null)
{
if (!guiSkins.ContainsKey(name))
{
GUISkin newSkin = RegisterDerived(name, baseSkin);
skinInitializer?.Invoke(newSkin);
}
return guiSkins[name];
}
/// <summary>
/// Switches the active skin until after the <paramref name="render"/> action.
/// </summary>
/// <param name="skin">Skin to switch to.</param>
/// <param name="render">Render function to run.</param>
public static void RenderWithSkin(GUISkin skin, Action render)
{
Validate.NotNull(render);
GUISkin prevSkin = GUI.skin;
GUI.skin = skin;
render();
GUI.skin = prevSkin;
}
/// <summary>
/// Adds or sets a custom style to the skin.
/// </summary>
/// <remarks>
/// Custom skins can be used by passing the "nameofskin" to the <see cref="GUIStyle"/> parameter of <see cref="GUILayout.Label(string, GUIStyle, GUILayoutOption[])"/> and similar.
/// </remarks>
/// <param name="skin">Skin to add a custom style to.</param>
/// <param name="name">Name of the new custom style.</param>
/// <param name="baseStyle">Style to base your custom style on.</param>
/// <param name="modify">Function that changes the custom style to your liking.</param>
public static void SetCustomStyle(this GUISkin skin, string name, GUIStyle baseStyle, Action<GUIStyle> modify)
{
GUIStyle style = new GUIStyle(baseStyle);
style.name = name;
modify(style);
int index = Array.FindIndex(skin.customStyles, item => item.name == style.name);
if (index >= 0)
{
skin.customStyles[index] = style;
}
else
{
// Increase array size and add style.
List<GUIStyle> styles = new List<GUIStyle>(skin.customStyles)
{
style
};
skin.customStyles = styles.ToArray();
}
}
}
}

View File

@@ -0,0 +1,181 @@
using System;
using System.Text;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
using Object = UnityEngine.Object;
namespace NitroxClient.Unity.Helper
{
public static class GameObjectHelper
{
public static bool TryGetComponentInChildren<T>(this GameObject go, out T component, bool includeInactive = false) where T : Component
{
component = go.GetComponentInChildren<T>(includeInactive);
return component;
}
public static bool TryGetComponentInParent<T>(this GameObject go, out T component, bool includeInactive = false) where T : Component
{
component = go.GetComponentInParent<T>(includeInactive);
return component;
}
public static bool TryGetComponentInChildren<T>(this Component co, out T component, bool includeInactive = false) where T : Component => TryGetComponentInChildren(co.gameObject, out component, includeInactive);
public static bool TryGetComponentInParent<T>(this Component co, out T component, bool includeInactive = false) where T : Component => TryGetComponentInParent(co.gameObject, out component, includeInactive);
public static T RequireComponent<T>(this GameObject o) where T : Component
{
T component = o.GetComponent<T>();
Validate.IsTrue(component, $"{o.name} did not have a component of type {typeof(T)}");
return component;
}
public static T RequireComponentInChildren<T>(this GameObject o, bool includeInactive = false) where T : Component
{
T component = o.GetComponentInChildren<T>(includeInactive);
Validate.IsTrue(component, $"{o.name} did not have a component of type {typeof(T)} in its children");
return component;
}
public static T RequireComponentInParent<T>(this GameObject o) where T : Component
{
T component = o.GetComponentInParent<T>();
Validate.IsTrue(component, $"{o.name} did not have a component of type {typeof(T)} in its parent");
return component;
}
public static T RequireComponent<T>(this Component co) where T : Component => RequireComponent<T>(co.gameObject);
public static T RequireComponentInChildren<T>(this Component co, bool includeInactive = false) where T : Component => RequireComponentInChildren<T>(co.gameObject, includeInactive);
public static T RequireComponentInParent<T>(this Component co) where T : Component => RequireComponentInParent<T>(co.gameObject);
public static Transform RequireTransform(this Transform tf, string name)
{
Transform child = tf.Find(name);
if (!child)
{
throw new ArgumentNullException($@"{tf} does not contain ""{name}""");
}
return child;
}
public static Transform RequireTransform(this GameObject go, string name) => go.transform.RequireTransform(name);
public static Transform RequireTransform(this MonoBehaviour mb, string name) => mb.transform.RequireTransform(name);
public static GameObject RequireGameObject(this Transform tf, string name) => tf.RequireTransform(name).gameObject;
public static GameObject RequireGameObject(this GameObject go, string name) => go.transform.RequireGameObject(name);
public static GameObject RequireGameObject(this MonoBehaviour mb, string name) => mb.transform.RequireGameObject(name);
public static GameObject RequireGameObject(string name)
{
GameObject go = GameObject.Find(name);
Validate.IsTrue(go, $"No global GameObject found with {name}!");
return go;
}
public static string GetFullHierarchyPath(this Component component)
{
return component ? $"{component.gameObject.GetFullHierarchyPath()} -> {component.GetType().Name}.cs" : "";
}
public static string GetHierarchyPath(this GameObject go, GameObject end)
{
if (!go || go == end)
{
return "";
}
if (!go.transform.parent)
{
return go.name;
}
StringBuilder sb = new(go.name);
for (GameObject gameObject = go.transform.parent.gameObject;
gameObject && gameObject != end;
gameObject = gameObject.transform.parent ? gameObject.transform.parent.gameObject : null)
{
sb.Insert(0, $"{gameObject.name}/");
}
return sb.ToString();
}
public static Transform GetRootParent(this Component co) => co.transform.GetRootParent();
public static Transform GetRootParent(this GameObject go) => go.transform.GetRootParent();
public static Transform GetRootParent(this Transform root)
{
while (root.parent)
{
root = root.parent;
}
return root;
}
public static bool TryGetComponentInAscendance<T>(this Transform transform, int degree, out T component)
{
while (degree > 0)
{
if (!transform.parent)
{
component = default;
return false;
}
transform = transform.parent;
degree--;
}
return transform.TryGetComponent(out component);
}
/// <summary>
/// Custom wrapper for prefab spawning which ensures a NitroxEntity is present
/// on the newly created object before its components are enabled (Awake is not fired).
/// </summary>
public static GameObject InstantiateInactiveWithId(GameObject original, NitroxId nitroxId, Vector3 position = default, Quaternion rotation = default)
{
GameObject copy = Object.Instantiate(original, position, rotation, false);
NitroxEntity.SetNewId(copy, nitroxId);
return copy;
}
/// <inheritdoc cref="InstantiateInactiveWithId(GameObject, NitroxId, Vector3, Quaternion)"/>
/// <remarks>
/// Sets the GameObject to active after spawning it with a NitroxEntity.
/// </remarks>
public static GameObject InstantiateWithId(GameObject original, NitroxId nitroxId, Vector3 position = default, Quaternion rotation = default)
{
GameObject copy = InstantiateInactiveWithId(original, nitroxId, position, rotation);
copy.SetActive(true);
return copy;
}
/// <summary>
/// Override for <see cref="Utils.CreateGenericLoot"/> using our own <see cref="InstantiateWithId"/> wrapper
/// </summary>
public static GameObject CreateGenericLoot(TechType techType, NitroxId nitroxId)
{
GameObject gameObject = SpawnFromPrefab(Utils.genericLootPrefab, nitroxId);
gameObject.GetComponent<Pickupable>().SetTechTypeOverride(techType, lootCube: true);
return gameObject;
}
/// <summary>
/// Override for <see cref="Utils.SpawnFromPrefab"/> using our own <see cref="InstantiateWithId"/> wrapper
/// </summary>
public static GameObject SpawnFromPrefab(GameObject prefab, NitroxId nitroxId, Transform parent = null)
{
GameObject gameObject = InstantiateWithId(prefab, nitroxId);
gameObject.transform.parent = parent;
return gameObject;
}
}
}

View File

@@ -0,0 +1,22 @@
using UnityEngine;
namespace NitroxClient.Unity.Helper
{
public class MathUtil
{
public static Vector3 ClampMagnitude(Vector3 v, float max, float min)
{
double sm = v.sqrMagnitude;
if (sm > (double)max * max)
{
return v.normalized * max;
}
else if (sm < (double)min * min)
{
return v.normalized * min;
}
return v;
}
}
}

View File

@@ -0,0 +1,134 @@
using System.Collections;
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap;
using UnityEngine;
using UnityEngine.UI;
namespace NitroxClient.Unity.Helper;
public static class RendererHelpers
{
//This entire method is necessary in order to deal with the fact that UWE compiles Subnautica in a mode
//that prevents us from accessing the pixel map of the 2D textures they apply to their materials.
public static Texture2D Clone(this Texture2D sourceTexture)
{
// Create a temporary RenderTexture of the same size as the texture
RenderTexture tmp = RenderTexture.GetTemporary(
sourceTexture.width,
sourceTexture.height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear);
// Blit the pixels on texture to the RenderTexture
Graphics.Blit(sourceTexture, tmp);
// Backup the currently set RenderTexture
RenderTexture previous = RenderTexture.active;
// Set the current RenderTexture to the temporary one we created
RenderTexture.active = tmp;
// Create a new readable Texture2D to copy the pixels to it
Texture2D clonedTexture = new(sourceTexture.width, sourceTexture.height);
// Copy the pixels from the RenderTexture to the new Texture
clonedTexture.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0);
clonedTexture.Apply();
// Reset the active RenderTexture
RenderTexture.active = previous;
// Release the temporary RenderTexture
RenderTexture.ReleaseTemporary(tmp);
return clonedTexture;
// "clonedTexture" now has the same pixels from "texture" and it's readable.
}
//This applies a color filter to a specific region of a 2D texture.
public static void SwapTextureColors(
this Texture2D texture,
HsvSwapper filter,
TextureBlock textureBlock)
{
Color[] pixels = texture.GetPixels(textureBlock.X, textureBlock.Y, textureBlock.BlockWidth, textureBlock.BlockHeight);
filter.SwapColors(pixels);
texture.SetPixels(textureBlock.X, textureBlock.Y, textureBlock.BlockWidth, textureBlock.BlockHeight, pixels);
texture.Apply();
}
public static void UpdateMainTextureColors(this Material material, Color[] pixels)
{
Texture2D mainTexture = (Texture2D)material.mainTexture;
mainTexture.SetPixels(pixels);
mainTexture.Apply();
}
//This applies a color filter to a specific region of a 2D texture.
public static void UpdateMainTextureColors(
this Material material,
Color[] pixels,
//IColorSwapStrategy colorSwapStrategy,
TextureBlock textureBlock)
{
Texture2D mainTexture = (Texture2D)material.mainTexture;
//Color[] pixels = mainTexture.GetPixels(textureBlock.X, textureBlock.Y, textureBlock.BlockWidth, textureBlock.BlockHeight);
//pixelIndexes.ForEach(pixelIndex => pixels[pixelIndex] = colorSwapStrategy.SwapColor(pixels[pixelIndex]));
mainTexture.SetPixels(textureBlock.X, textureBlock.Y, textureBlock.BlockWidth, textureBlock.BlockHeight, pixels);
mainTexture.Apply();
}
public static void ApplyClonedTexture(this Material material)
{
Texture2D mainTexture = (Texture2D)material.mainTexture;
Texture2D clonedTexture = mainTexture.Clone();
material.mainTexture = clonedTexture;
}
public static SkinnedMeshRenderer GetRenderer(this GameObject playerModel, string equipmentGameObjectName)
{
return playerModel
.transform
.Find(equipmentGameObjectName)
.gameObject
.GetComponent<SkinnedMeshRenderer>();
}
public static Color[] GetMainTexturePixels(this Material material)
{
Texture2D mainTexture = (Texture2D)material.mainTexture;
return mainTexture.GetPixels();
}
public static Color[] GetMainTexturePixelBlock(
this Material material,
TextureBlock textureBlock)
{
Texture2D mainTexture = (Texture2D)material.mainTexture;
return mainTexture.GetPixels(textureBlock.X, textureBlock.Y, textureBlock.BlockWidth, textureBlock.BlockHeight);
}
/// Copied from MainMenuLoadButton.ShiftAlpha()
public static IEnumerator ShiftAlpha(
this CanvasGroup cg,
float targetAlpha,
float animTime,
float power,
bool toActive,
Selectable buttonToSelect = null)
{
float start = Time.time;
while (Time.time - start < animTime)
{
cg.alpha = Mathf.Lerp(cg.alpha, targetAlpha, Mathf.Pow(Mathf.Clamp01((Time.time - start) / animTime), power));
yield return null;
}
cg.alpha = targetAlpha;
if (toActive)
{
cg.interactable = true;
cg.blocksRaycasts = true;
}
else
{
cg.interactable = false;
cg.blocksRaycasts = false;
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Text;
using NitroxModel.Helper;
namespace NitroxClient.Unity.Helper
{
public static class StringUtils
{
public static string TruncateRight(this string value, int maxChars, string appendix = "...")
{
Validate.NotNull(value);
Validate.NotNull(appendix);
return value.Length <= maxChars ? value : value.Substring(0, maxChars) + appendix;
}
public static string TruncateLeft(this string value, int maxChars, string appendix = "...")
{
Validate.NotNull(value);
Validate.NotNull(appendix);
return value.Length <= maxChars ? value : appendix + value.Substring(value.Length - maxChars, maxChars);
}
public static string ByteArrayToHexString(this byte[] bytes)
{
StringBuilder hex = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
{
hex.Append("0x");
hex.Append(b.ToString("X2"));
hex.Append(" ");
}
return hex.ToString();
}
}
}

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace NitroxClient.Unity.Helper;
/// <summary>
/// Cache for yields in IEnumeration to reduce GC pressure.
/// </summary>
public static class Yielders
{
public static readonly WaitForFixedUpdate WaitForFixedUpdate = new();
public static readonly WaitForEndOfFrame WaitForEndOfFrame = new();
public static readonly WaitForSeconds WaitFor100Milliseconds = new(.1f);
public static readonly WaitForSeconds WaitForHalfSecond = new(.5f);
public static readonly WaitForSeconds WaitFor1Second = new(1);
public static readonly WaitForSeconds WaitFor2Seconds = new(2);
public static readonly WaitForSeconds WaitFor3Seconds = new(3);
}