ATVO by Appgineering
Download
Read-only

Correct Syntax for a Few Things - Question

53 posts 28,569 views Started 02 Feb 2020, 21:59
Showing 1–53 of 53 posts
Kyle H.
Original poster

Hi guys,

Making the first major updates to our theme in quite some time. I have a script that returns the live gap, PIT, OUT, etc, normal things.

I'd now like to incorporate manufacturer logos in place of this data on occasion via a button press. Could you explain the syntax for how I could achieve pressing a button, then loading an image in place of text in the relevant label? I imagine I'd have to return " " in the label, to clear it, and then turn on another label with the manufacturer image loaded over it. Once I click another button, I'd like the script to return to giving gap data.

Previously I had just loaded another ticker in an adjacent column, which worked nicely but was a little clunky looking. Thank you for your help!

Kyle Heyer

Nick Thissen Appgineering
Reply #1

I'm assuming these labels are in a ticker? Because that complicates matters a little bit.

In principle (ignoring the ticker aspect for now), the logic is pretty simple. I would simply build two labels on top of each other, one with the text and one with the image. Then in a script you can switch between the two by making one label invisible and the other visible. Probably something very close to this:

var labelText = subwidget.Labels.Find("TextLabel");
var labelImg = subwidget.Labels.Find("ImageLabel");

// Switch from text to image:
labelText.IsVisible = false;
labelImg.IsVisible = true;

For a ticker, you will need to do this for every copy of the labels in the ticker. For your understand, the ticker works by making several copies of the "template" subwidget (which you select in the properties). If there are 26 drivers, it will create 26 copies of that template. These copies are used until the ticker decides it needs to rebuild the copies (which happens sometimes, e.g. when number of drivers change). Simply changing the template subwidget after the fact will not impact the copies already made.

So to change all labels in a ticker you'll have to loop through all copies of the subwidgets and change them. And at the same time, I would also change the template itself, so that in the event that the copies are rebuilt, they are rebuilt using the current desired state.

The copies of the template are available from the ticker via the 'RepeatedSubWidgets' property. You can loop over those and change the properties, and then also change them for the template itself.

There is one tricky complication: the labels in a copy of the template do not get a name, so you cannot use the standard "subwidget.Find" method to get them. You can instead just get them by index but you'd need to find out the index first, and take care that it does not change (e.g. if you add or move around labels in the template subwidget).

Assuming the labels are the first and second (index 0 and 1), this should work I think:

using System;
using ATVO.ThemesSDK;
using ATVO.ThemeEditor.ThemeModels;
using ATVO.ThemeEditor.Scripting.DotNET;

namespace Scripts
{
	public class ChangeTickerImg : IScript
	{
		public object Execute(ThemeContentItem item, object value, string parameter, ISimulation sim)
		{
			var widget = item.Theme.Widgets.Find("Widget1");
			
			foreach (var subwidget in widget.Ticker.RepeatedSubWidgets)
			{
				ChangeLabels(subwidget);
			}
			ChangeLabels(widget.Ticker.TemplateSubWidget);
			
			return null;
		}
		
		private void ChangeLabels(SubWidget subwidget)
		{
			var labelText = subwidget.Labels[0];
			var labelImg = subwidget.Labels[1];
			
			labelText.IsVisible = false;
			labelImg.IsVisible = true;
		}
	}
}
Kyle H.
Reply #2
· edited

Thanks Nick! This will give me a good direction to go. How do I make a button in the controls trigger the swap in the script? Just by using Action type "Script" with the "Execute" effect?

Edit I discovered that the way above is the correct way to trigger the label change. I have it working nicely - except all the labels sit on top of each other until I click the button to run the script. I'll make another script to initialize the ticker to show only the label I want on startup.

Edit 2

It seems every time the gap data updates, it sets itself visible again. I assume this is because my button runs the script once, and then it returns back to the default status of everything being visible when the ticker runs through again.

Appreciate the help as always!

Kyle

Nick Thissen Appgineering
Reply #3

It may be the 'Dynamic' property messing with the visibility when the text goes from empty to some value. Instead of making it invisible perhaps you can just set the text of the label to empty ("") to hide it, and back to the default "{0:0}" (or whatever formatting you want) to show it again.

Kyle H.
Reply #4

Nick Thissen wrote:
It may be the 'Dynamic' property messing with the visibility when the text goes from empty to some value. Instead of making it invisible perhaps you can just set the text of the label to empty ("") to hide it, and back to the default "{0:0}" (or whatever formatting you want) to show it again.

This pretty much worked, for some reason it doesn't work with something entirely empty, but as long as there's some form of character in there it works fine. Perhaps I could set the color to transparent and that would achieve the same effect. I'll give that a go. Great progress - thanks!!!

Kyle H.
Reply #5

Got it working exactly how I envisioned. This is a tad messier since it was more than just car manufacturer I wanted to show, so I have to turn off more than just one label.

Thank you so much!

using System;
using ATVO.ThemesSDK;
using ATVO.ThemeEditor.ThemeModels;
using ATVO.ThemeEditor.Scripting.DotNET;
using System.Drawing;
using System.Windows.Media;

namespace Scripts
{
	public class Manufacturer : IScript
	{
		public object Execute(ThemeContentItem item, object value, string parameter, ISimulation sim)
		{
			var widget = item.Theme.Widgets.Find("Top TickerB");
			var widget2 = item.Theme.Widgets.Find("Ticker");
			var widget3 = item.Theme.Widgets.Find("Ticker - Scrolling");
			
			foreach (var subwidget in widget.Ticker.RepeatedSubWidgets)
			{
				ChangeLabels(subwidget);
			}
			
			foreach (var subwidget in widget2.Ticker.RepeatedSubWidgets)
			{
				ChangeLabels(subwidget);
			}
			
			foreach (var subwidget in widget3.Ticker.RepeatedSubWidgets)
			{
				ChangeLabels(subwidget);
			}
			ChangeLabels(widget.Ticker.TemplateSubWidget);
			ChangeLabels(widget2.Ticker.TemplateSubWidget);
			ChangeLabels(widget3.Ticker.TemplateSubWidget);
			
			return null;
		}
		
		private void ChangeLabels(SubWidget subwidget)
		{
			Color OFF = Color.FromArgb(0, 0, 0, 0);
			Color ON = Color.FromArgb(255, 255, 255, 255);
			
			var labelText = subwidget.Labels[2];
			var labelImg = subwidget.Labels[6];
			var labelStart = subwidget.Labels[7];
			
			labelText.Text = ".";
			labelText.Font.FontColor = OFF;
			
			labelImg.Text = "{0:0}";
			labelImg.Font.FontColor = ON;
			
			labelStart.Text = ".";
			labelStart.Font.FontColor = OFF;
		}
	}
}
Emmanuel S.
Reply #6

Hi,
I really don't know how to use scripts correctly but I try ...
I want to check if the session is RACE or not to mask a the label wich inform the number of lap in race.
This label is in Widget with dataset SESSION STATE.
I try to adapt a script I see here but it must be use with other dataset ...

using System; 
using ATVO.ThemesSDK; 
using ATVO.ThemesSDK.Data.Entity; 
using ATVO.ThemeEditor.ThemeModels; 
using ATVO.ThemeEditor.Controls; 
using ATVO.ThemeEditor.Scripting.DotNET;
using ATVO.ThemesSDK.Data.Enums;
using ATVO.ThemesSDK.Data.Results;
namespace Scripts
{
	public class Script : IScript
	{
		public object Execute(ThemeContentItem item, object value,string parameter,  ISimulation sim)
		{
			// Bind to "entitysessionresult_obj" to get the IEntitySessionResult as the value

			// Cast to IEntitySessionResult type
			IEntitySessionResult sessionResult = (IEntitySessionResult) sessionResult;
			
		
			var type = result.Session.Type;
			
			if (type == SessionType.Race)
			
					return "Lap"; 
				else 
					return "";
			}
			
		}
	
}

I use data converter, but I can't set entitysessionresult_obj in this data set.

Nick Thissen Appgineering
Reply #7

If you use 'sessionstate' dataset, then the databindings you can use are different. In that case you can bind directly to "sessiontype" binding, which returns "Practice", "Qualification", "Warmup" or "Race".

If you bind to that, your "value" in the script will be a string and you can directly compare if it equals Race:

if (value == "Race")
Emmanuel S.
Reply #8
· edited

Thanks, I do (only 3 hours to understand .... :(  )
With that code it is ok

using System;
using ATVO.ThemesSDK;
using ATVO.ThemeEditor.ThemeModels;
using ATVO.ThemeEditor.Scripting.DotNET;

namespace Scripts
{
 public class Scripts1 : IScript
 {
 public object Execute(ThemeContentItem item, object value, string parameter, ISimulation sim)
 {
 // Get databinding value as string:
 string sessionname = value.ToString();
 
if (sessionname == "Practice")
 return ""; 

if (sessionname == "Race")
 return "Lap"; 
 
else 
 return value;
 

 }
 }
}

I put value for else to check what hapen !

Now I try do the same with a non text label : currentlap
I look what I have to change ...

Emmanuel S.
Reply #9
· edited

Finaly I decide to add a SubWidget only to do a masquing image. One to mask area and other totally clear to see information when sessiontype = race.

Emmanuel S.
Reply #10

I make some test and I do a little script which show fast lap or delta depend if sessiontype is race or not.
Now I want to find how if it is a Multiclass to change the label I use to indicate CLASS, to change text to "Multiclass" .
I don't find that !

Nick Thissen Appgineering
Reply #11

You can count the number of classes in the session via the sim object:

var numClasses = sim.Session.ClassManager.Classes.Count;

Then simply check if numClasses > 1 (multiclass) or not (single class).

Emmanuel S.
Reply #12

Thanks it is ok and I can change what I want. I try to find on GitHub some variable, but it is not so easy ;)
if I want to say give the good name else, I don't find the fonction ... Class.Name or other ...

Other things, I do a DROPDOWN to select wich standing class to show. When we choose class with no cars it's freeze the computer for long time. So I try to imput a condition that if I got 3 classes I can show only class 1/2 & 3 and not class4.
I don't see how Do a Condition if a label has a specific value !

Emmanuel S.
Reply #13

Just to understand, in GitHub it is old system ?
The complet name to use are in dll at C:\Program Files (x86)\Appgineer.in\ATVO Launcher\bin ?

Nick Thissen Appgineering
Reply #14

The Github is relatively old yes, but there are only additions which are not yet in the Github, and no other changes.
We will try to update it soon.

There is also a nuget package which is referenced automatically if you open the scripts in VS Code (via right-click on Scripts - Open in VS Code). This is also not fully up to date currently, but we'll update that soon.

Why are you looking for the DLL?

Emmanuel S.
Reply #15
· edited

As you can see I'm not programer, but I try, so First I want to fing correct name of "variable".

For exemple in GitHub I saw the name Class and you give me Classes. So I only try to undernstand.

I use Visual Studio, but yes, no all name :)

Emmanuel S.
Reply #16

Hi Nick, i saw a post where you describe how to change color of delta time if it under a value. But i can't find it again !! Could you give me the path ?

Emmanuel S.
Reply #17

Nick Thissen wrote:
You can count the number of classes in the session via the sim object:

var numClasses = sim.Session.ClassManager.Classes.Count;

Then simply check if numClasses > 1 (multiclass) or not (single class).

Ok with that, I get the number of classes and I use it.

But if I want to check this car is in the first class, nomaly I have to check Classes[0] , yes ?

Something like If (Classes[0] == true) ?

Emmanuel S.
Reply #19

Name on Data Explorer is classorder
0 is the first ....

Nick Thissen Appgineering
Reply #20

Looks like you need Order and not Id indeed. Then I think Order goes from 0 - 6 (or however many classes), and Id is probably the ClassId as defined by iRacing.

You can see the "classorder" binding in the Data Explorer, but you can also use "Expand object" on an "..._object" type to see what properties are available from a script. For example, expanding the "entitysessionresult_object" you can find Entity and then Car and then Class and then you can see the properties:


Nick Thissen Appgineering
Reply #22
· edited

Yes, that will be 'true' if the car is in the fastest class.

Note that the order of classes (by how fast they are) is defined by iRacing, unless you use custom classes.

Emmanuel S.
Reply #23
· edited

Yes, I try with 3 classes and it is ok. (this is in relation with override color question ;) )

Nick Thissen Appgineering
Reply #24

Are you just trying to replace the font color of a class? You can do that with the custom classes setting in ATVO, no need to use overrides for this.

Emmanuel S.
Reply #25

Not Font but color class, yes ..... :(

Opps, I have to look how it works .....

But I understand some good tips to do script ....

Sorry to get time for that !

Emmanuel S.
Reply #26
· edited

Ok, I test it before but I don't understand what to do.
Tonight, I pass some time and I finally understand and manage a personnal class. It is what I need.

Emmanuel S.
Reply #27
· edited

At Nick, finally I decide to use a combinaison beetween scripts and CustomsClasses.
I use a Ticker and decide to override color class by iracing or CustomClasses, but if only one class I want to have other color than White.
I done this and it is ok ;)
Binding is classcolor

using System;
using ATVO.ThemesSDK;
using ATVO.ThemeEditor.ThemeModels;
using ATVO.ThemeEditor.Scripting.DotNET;
using System.Windows.Media;
using ATVO.ThemesSDK.Data.Entity;
using ATVO.ThemesSDK.Data.Results;

namespace Scripts
{
 public class SC_ClassColor : IScript
 {
 public object Execute(ThemeContentItem item, object value, string parameter, ISimulation sim)
 {
 if (value == null)
 {
     // do something appropriate, in this case probably just return nothing
     return null;
 } 
var Classcolor = value.ToString();
 var numClasses = sim.Session.ClassManager.Classes.Count;
 var hex = "#17a5ff";

 if (numClasses == 1)
 return (Color) ColorConverter.ConvertFromString(hex);
 else 
 return (Color) ColorConverter.ConvertFromString(Classcolor);
 }
 }
}
Emmanuel S.
Reply #28

I try to get different information from different places but in the same widget (so with same data set)

I want to add the track name in combinaison with classname for result title.

I try to catch information with something like that:

IEntitySessionResult result = (IEntitySessionResult) value;
			ITrack result1 = (ITrack) value;
			
			var classname = result.Entity.Car.Class.Name;
			var track0 = result1.DisplayName;

return track0 + "  " + classname;

But track0 return no good value.

Emmanuel S.
Reply #29

Other things.
Is it possible to change Data Class filter of a widget ?
For some ticker or resluts, it will be intersting to just change that for show standing class1 or class2 or all.

Nick Thissen Appgineering
Reply #30

Emmanuel S. wrote:
I try to get different information from different places but in the same widget (so with same data set)

I want to add the track name in combinaison with classname for result title.

I try to catch information with something like that:

IEntitySessionResult result = (IEntitySessionResult) value;
 ITrack result1 = (ITrack) value;
 
 var classname = result.Entity.Car.Class.Name;
 var track0 = result1.DisplayName;

return track0 + "  " + classname;

But track0 return no good value.

No, it doesn't work that way. I think you misunderstand the way script converters work.

The "value" parameter that the script receives is the value of the data binding you selected. This can be a simple value like a driver name, carnumber, etc. Or it can be an object if you select for example "entitysessionresult_object" or "entity_object" as the binding. In the end, the type of value is only one type. The first line in most of the scripts is called a 'cast', where you tell the compiler that the type of value is a certain type. For example, this tells the compiler "treat value as type IEntitySessionResult":

var result = (IEntitySessionResult) value;

It's important to realize there is no conversion or anything going on here, it is just telling the compiler what the type will be. If value was actually another type, this will fail.

You can't cast value to an IEntitySessionResult and then to an ITrack object; those are different types.

Long story short, to get the track you can find it via the sim.Session property:

ITrack track = sim.Session.Track;
Nick Thissen Appgineering
Reply #31
· edited

Emmanuel S. wrote:
Other things.
Is it possible to change Data Class filter of a widget ?
For some ticker or resluts, it will be intersting to just change that for show standing class1 or class2 or all.

You can change the DataClassIndex property of the widget just by specifying a new value. The type must be one of the available values in the DataClassIndex enum. The possible values are:

  • DataClassIndex.FollowedClass = -1
  • DataClassIndex.AllClasses= 0
  • DataClassIndex.Class1 = 1
  • DataClassIndex.Class2 = 2
  • etc...
  • DataClassIndex.Class10 = 10
using System;
using ATVO.ThemesSDK;
using ATVO.ThemeEditor.ThemeModels;
using ATVO.ThemeEditor.Scripting.DotNET;
using ATVO.ThemeEditor.Data;

namespace Scripts
{
 public class ChangeClassIndex : IScript
 {
 public object Execute(ThemeContentItem item, object value, string parameter, ISimulation sim)
 {
 var widget = item.Theme.Widgets.Find("Widget1");
 
 // Change to FollowedClass
 widget.DataClassIndex = DataClassIndex.FollowedClass;
 
 // Change to class 2
 widget.DataClassIndex = DataClassIndex.Class2;
 
 // Change to class "n" where n is an integer 1-10
 var n = 5;
 widget.DataClassIndex = (DataClassIndex)n; 
 
 return null;
 }
 }
}

I may add new options to change a class filter via actions in the ChangeDataSource actions, instead of requiring a script.

Emmanuel S.
Reply #33

To find Dropdowns it seems that:

var input = item.Theme.Dropdowns.Find("DD_Class_select");

But is it possible to know what is selected in Dropdown ?

Nick Thissen Appgineering
Reply #34

You can get the SelectedItem. With the SelectedItem you can also get the Text, or you can get the index by using the IndexOf function on the list of items.

The upcoming ATVO theme uses this to change the class filter on tickers. It is slightly more complicated as it is designed to be re-used for multiple tickers so the first part of the script is finding the right widget to change. The rest should be similar.

This is triggered by a dropdown which has the items in this order:

  • Index 0: All classes
  • Index 1: Class 1
  • Index 2: Class 2
  • etc...
using System;
using ATVO.ThemesSDK;
using ATVO.ThemeEditor.ThemeModels;
using ATVO.ThemeEditor.Scripting.DotNET;
using ATVO.ThemeEditor.Data;
using System.Collections.Generic;

namespace Scripts
{
	public class ChangeTickerClassFilter : IScript
	{
		public object Execute(ThemeContentItem item, object value, string parameter, ISimulation sim)
		{
			// Which ticker(s) are we changing?
			List<Widget> tickers = new List<Widget>();
			var tickerValue = (string)value;
			if (tickerValue == "H")
			{
				// Horizontal ticker
				tickers.Add(item.Theme.Widgets.Find("W_HTicker"));
			}
			else if (tickerValue == "V")
			{
				// Vertical ticker
				tickers.Add(item.Theme.Widgets.Find("W_VTickerTop"));
				tickers.Add(item.Theme.Widgets.Find("W_VTickerBottom"));
			}
			else if (tickerValue == "Grid")
			{
				// Grid tickers
				tickers.Add(item.Theme.Widgets.Find("W_GridLeft"));
				tickers.Add(item.Theme.Widgets.Find("W_GridRight"));				
			}
			else if (tickerValue == "Results")
			{
				// Results ticker
				tickers.Add(item.Theme.Widgets.Find("W_Results"));
			}
			
			// Default: all classes
			DataClassIndex index = DataClassIndex.AllClasses;

			// Find the dropdown and the index of the selected item
			var dropdown = (Dropdown)item;
			var selectedIndex = dropdown.Items.IndexOf(dropdown.SelectedItem);
			if (selectedIndex >= 0)
			{
				// The index of the dropdown items happens to coincide with the class filter indices, 
				// so this is an easy way to set the index without a long list of "if" or switch statements
				index = (DataClassIndex) selectedIndex;
			}

			// Finally change the class filter
			// Also restart the ticker to have it recalculate which items to show
			foreach (var ticker in tickers)
			{
				ticker.DataClassIndex = index;	
				ticker.Ticker.Restart();
			}
			
			return null;
		}
	}
}
Emmanuel S.
Reply #35
· edited

And if I want when I change Class, to use Class Position than Position (when i am not in AllClasses), we need to change DataOrder, but the name not match in my script.

Emmanuel S.
Reply #36

Hi.
Does it something for showing witch iRacing Split is watching ?

Emmanuel S.
Reply #37

Is it possible to search Storyboards in a scripts ?

var sybd = item.Theme.Storyboards.Find("SB1"); returns that it is not a function

Nick Thissen Appgineering
Reply #38

Emmanuel S. wrote:
Hi.
Does it something for showing witch iRacing Split is watching ?

No, we don't get that info.

Emmanuel S. wrote:
Is it possible to search Storyboards in a scripts ?

var sybd = item.Theme.Storyboards.Find("SB1");  returns that it is not a function

Where does it say that? It seems to work fine for me.

Emmanuel S.
Reply #39

For stroryboard, error is when I want to make:
sybd.Restart();
Restart is not a definition of Storyboard

Nick Thissen Appgineering
Reply #40

Make sure you're on the latest stable or beta channel. Alpha channel doesn't have storyboards.

Emmanuel S.
Reply #42
· edited

Emmanuel S. wrote:
And if I want when I change Class, to use Class Position than Position (when i am not in AllClasses), we need to change DataOrder, but the name not match in my script.

This is for Ticker with DropDown selection:

Is it possible to add in script to change to Dataorder --> liveposition if index=0 and Dataorder --> liveclassposition if index >= 1 ?

Nick Thissen Appgineering
Reply #43

Emmanuel S. wrote:
For stroryboard, error is when I want to make:
sybd.Restart();
Restart is not a definition of Storyboard

There is no Restart indeed. Just do Stop, then Play to start a new instance:

sybd.Stop(); // stops all instances of this storyboard
sybd.Play(); // starts a new instance

Emmanuel S. wrote:
Emmanuel S. wrote:
And if I want when I change Class, to use Class Position than Position (when i am not in AllClasses), we need to change DataOrder, but the name not match in my script.

This is for Ticker with DropDown selection:

Is it possible to add in script to change to Dataorder --> liveposition if index=0 and Dataorder --> liveclassposition  if index >= 1 ?

You can use this:

widget.DataOrder = DataSetManager.GetDataOrder("liveclassposition");
Emmanuel S.
Reply #44

Thanks, it is ok I add it to your code

var dropdown = (Dropdown)item;
			var selectedIndex = dropdown.Items.IndexOf(dropdown.SelectedItem);
			var dataO = "";
			if (selectedIndex == 0)
			{
				// The index of the dropdown items happens to coincide with the class filter indices, 
				// so this is an easy way to set the index without a long list of "if" or switch statements
				index = (DataClassIndex) selectedIndex;
				dataO ="liveposition";
				
			}
			if (selectedIndex >= 1)
			{
				// The index of the dropdown items happens to coincide with the class filter indices, 
				// so this is an easy way to set the index without a long list of "if" or switch statements
				index = (DataClassIndex) selectedIndex;
				dataO ="liveclassposition";
			}

			// Finally change the class filter
			// Also restart the ticker to have it recalculate which items to show
			foreach (var ticker in tickers)
			{
			
				ticker.DataOrder = DataSetManager.GetDataOrder(dataO);
				ticker.DataClassIndex = index;	
				ticker.Ticker.Restart();
			}

Now I have to change info in the label position to position and not classpostion ;)
I look in your new theme, I see in the Driver info something help me.

Emmanuel S.
Reply #45

Nick Thissen wrote:
Emmanuel S. wrote:
For stroryboard, error is when I want to make:
sybd.Restart();
Restart is not a definition of Storyboard

There is no Restart indeed. Just do Stop, then Play to start a new instance:

sybd.Stop(); // stops all instances of this storyboard
sybd.Play(); // starts a new instance

Ok I just test Restart and Start, don't think to Play ;)

Emmanuel S.
Reply #46

Is it possible to get the number of pilot in each class ?

Nick Thissen Appgineering
Reply #47

If you want to count the number of drivers in a particular class (which you identify by ID or Order or whatever), you can use this:

var classId = 1;
var count = sim.Session.Entities.Where(e => e.Car.Class.Id == classId).Count();

If you want to do it for all classes, you can group the entities by class ID (or order) and count the number of items in each group:

// Group entities by class ID
var groups = sim.Session.Entities.GroupBy(e => e.Car.Class.Id);

// For each group (= class) get the ID and number of drivers
foreach (var group in groups)
{
	var classId = group.Key;
	var driverCount = group.Count();
}
Emmanuel S.
Reply #48

Try to get hour in script and THEME EDITOR don't want that value:

ISession result = (ISession) value;
var time = result.SessionOptions.DateTime.Hour;

It is the path I find in data explorer.

Nick Thissen Appgineering
Reply #49

For in-sim time we provide both the starting time and current (live) time. Starting time is available via the session options (it does not change), while live time is available from the telemetry object (it changes every update):

var startTime = sim.Session.Options.StartDateTime;
var startHour = startTime.Hour;

var currentTime = sim.Telemetry.LiveDateTime;
var currentHour = currentTime.Hour;
Archive · Read-only

New replies have moved to Discord.