├── Plugin_ClassMethodCoroutineTest.as ├── Plugin_CoroutineTest.as ├── Plugin_DrawTest.as ├── Plugin_NetworkTest.as ├── Plugin_SettingsAdvancedTest.as ├── Plugin_SettingsTest.as ├── Plugin_TableTest.as ├── Plugin_YieldTest.as └── Readme.md /Plugin_ClassMethodCoroutineTest.as: -------------------------------------------------------------------------------- 1 | #name "Class Method Coroutine example" 2 | #author "XertroV" 3 | #category "Examples" 4 | 5 | /* This plugin demonstrates how class methods can be used as coroutines. 6 | * Unlike other coroutines, a delegate object must be used instead of 7 | * the method itself. 8 | * 9 | * You should read Plugin_CoroutineTest.as before this file. 10 | * 11 | * AngelScript docs for function handles and delegates: 12 | * https://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_funcptr.html 13 | */ 14 | 15 | void Main() { 16 | // We need to instantiate the class. The class itself is defined below. 17 | // The class starts a class method coroutine in its constructor. 18 | CoroExampleCls@ myInstance = CoroExampleCls("abcd"); 19 | 20 | // Let's confirm that the coroutine really is bound to `myInstance`. 21 | sleep(500); 22 | print("changing myInstance.name to 1234\n"); 23 | myInstance.name = "1234"; 24 | 25 | // stop the coroutines after 1500 ms avoid polluting the log. 26 | sleep(1500); 27 | myInstance.stop = true; 28 | print("coroutines stopped."); 29 | } 30 | 31 | // Example class with coroutines 32 | class CoroExampleCls { 33 | // A property that we'll modify later to show that we really are 34 | // running class methods as coroutines. 35 | string name; 36 | 37 | // Used to terminate the coroutines 38 | bool stop = false; 39 | 40 | // A basic constructor 41 | CoroExampleCls(string name) { 42 | this.name = name; 43 | 44 | // To run a class method as a coroutine, we construct a *delegate* 45 | // using the `CoroutineFunc` funcdef (provided by OpenPlanet). 46 | startnew(CoroutineFunc(this.AClassCoroutine)); 47 | sleep(111); 48 | 49 | // This also works (no explicit `this.`). Note: there will be two 50 | // instances of this coroutine running, now. 51 | startnew(CoroutineFunc(AClassCoroutine)); 52 | sleep(111); 53 | 54 | // We can also start a coroutine that takes an argument. 55 | 56 | // This will be our argument. It'll be automatically cast to `ref@`. 57 | Meta::Plugin@ thisPlugin = Meta::ExecutingPlugin(); 58 | 59 | // We should use `CoroutineFuncUserdata` as the funcdef, this time. 60 | startnew(CoroutineFuncUserdata(AClassCoroWithArg), thisPlugin); 61 | } 62 | 63 | // A class method that is a coroutine with an infinite loop. 64 | void AClassCoroutine() { 65 | while (!stop) { 66 | print("AClassCoroutine() here with name=" + this.name); 67 | sleep(333); 68 | } 69 | } 70 | 71 | // A class method that is a coroutine with an infinite loop which takes an argument. 72 | void AClassCoroWithArg(ref@ _arg) { 73 | // We need to know the datatype here -- be careful that you don't pass in an argument of the wrong type. 74 | auto arg = cast(_arg); 75 | while (!stop) { 76 | print("AClassCoroWithArg() here with name=" + this.name + " -- run from plugin: " + arg.Name); 77 | sleep(333); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Plugin_CoroutineTest.as: -------------------------------------------------------------------------------- 1 | #name "Coroutine example" 2 | #author "Miss" 3 | #category "Examples" 4 | 5 | /* This plugin demonstrates how coroutines work and how they can be 6 | * created and managed. 7 | * 8 | * Coroutines can be looked at in a similar way as threads, except that 9 | * they are single-threaded. Whenever a routine yields or returns (also 10 | * see Plugin_YieldTest.as) it will give the script engine a chance to 11 | * run other coroutines from the plugin, as well as routines from other 12 | * plugins. 13 | * 14 | * Coroutines are not guaranteed to run at the same frequency, they are 15 | * however guaranteed to be executed parallel within the same thread. 16 | */ 17 | 18 | // This is our Main function. In itself, it is also a coroutine. 19 | void Main() 20 | { 21 | // We will start a new coroutine here, with a function defined below. 22 | startnew(MyCoroutine); 23 | 24 | // Next, we put this routine into an infinite loop. 25 | while (true) { 26 | print("Hello from Main()"); 27 | sleep(1000); 28 | } 29 | } 30 | 31 | // This is the function for the coroutine we created in Main(). 32 | void MyCoroutine() 33 | { 34 | // We are allowed to yield within this function, as it is a coroutine. 35 | // Now, let's start another infinite loop on a different frequency as 36 | // the loop in Main(). This will demonstrate that they are in fact 37 | // running in parallel. 38 | while (true) { 39 | print("MyCoroutine() here"); 40 | sleep(250); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Plugin_DrawTest.as: -------------------------------------------------------------------------------- 1 | #name "Draw example" 2 | #author "Miss" 3 | #category "Examples" 4 | 5 | /* This plugin is a simple demo of the drawing API. It draws a circle 6 | * and a rounded square on the screen, with a line in between, and the 7 | * text "Hello, world!" next to it. 8 | * 9 | * It moves this continously across the screen. 10 | */ 11 | 12 | // These are just some states we have to remember for our movement. 13 | uint64 g_fromTime; 14 | uint64 g_toTime; 15 | vec2 g_fromPoint; 16 | vec2 g_toPoint; 17 | float g_fromCircleRadius; 18 | float g_toCircleRadius; 19 | 20 | int g_interval = 500; 21 | 22 | // Easing function. 23 | float easeQuad(float x) 24 | { 25 | if ((x /= 0.5) < 1) return 0.5 * x * x; 26 | return -0.5 * ((--x) * (x - 2) - 1); 27 | } 28 | 29 | // Our Main function, which merely updates our states when needed. 30 | void Main() 31 | { 32 | // Draw::GetWidth() gets the width of the screen. If this returns -1, 33 | // the rendering isn't ready yet and you can't use the Draw API, so 34 | // we have to wait for that to be a valid value. 35 | while (Draw::GetWidth() == -1) { 36 | yield(); 37 | } 38 | 39 | g_toPoint = vec2( 40 | Draw::GetWidth() / 2.0f, 41 | Draw::GetHeight() / 2.0f 42 | ); 43 | 44 | while (true) { 45 | g_fromPoint = g_toPoint; 46 | 47 | vec2 newPoint; 48 | do { 49 | float newAngle = Math::Rand(0.0f, Math::PI * 2.0f); 50 | newPoint = g_fromPoint + vec2(Math::Cos(newAngle), Math::Sin(newAngle)) * 300.0f; 51 | } while (newPoint.x < 0 || newPoint.y < 0 || newPoint.x > Draw::GetWidth() || newPoint.y > Draw::GetHeight()); 52 | 53 | g_toPoint = newPoint; 54 | g_fromTime = Time::Now; 55 | g_toTime = g_fromTime + g_interval; 56 | 57 | g_fromCircleRadius = g_toCircleRadius; 58 | g_toCircleRadius = g_fromCircleRadius + 0.4f; 59 | 60 | // We sleep a bit to wait for the next needed change. 61 | sleep(g_interval); 62 | } 63 | } 64 | 65 | void Render() 66 | { 67 | if (g_toTime == 0) { 68 | return; 69 | } 70 | 71 | float timeFactor = Math::InvLerp(g_fromTime, g_toTime, Time::Now); 72 | 73 | vec4 colFill = vec4(1, 0.9f, 0.7f, 1); 74 | vec4 colBorder = vec4(1, 1, 1, 1); 75 | 76 | vec2 point = Math::Lerp(g_fromPoint, g_toPoint, easeQuad(timeFactor)); 77 | float circleRadius = Math::Lerp(g_fromCircleRadius, g_toCircleRadius, timeFactor); 78 | vec2 circlePos = point + vec2(Math::Cos(circleRadius), Math::Sin(circleRadius)) * 75.0f; 79 | 80 | // Draws a line. 81 | Draw::DrawLine(point, circlePos, colBorder, 2.0f); 82 | 83 | // Draws a filled rectangle with a border rounding of 8px, followed 84 | // by a border of 2px width and 8px border rounding. 85 | Draw::FillRect(vec4(point.x - 16, point.y - 16, 32, 32), colFill, 8.0f); 86 | Draw::DrawRect(vec4(point.x - 16, point.y - 16, 32, 32), colBorder, 8.0f, 2.0f); 87 | 88 | // Draws a filled circle, followed by a border of 2px width. 89 | Draw::FillCircle(circlePos, 16.0f, colFill); 90 | Draw::DrawCircle(circlePos, 16.0f, colBorder, 2.0f); 91 | 92 | // Draws the string "Hello, world!" below the square position. 93 | Draw::DrawString(point + vec2(0, 24), colBorder, "Hello, world!"); 94 | } 95 | -------------------------------------------------------------------------------- /Plugin_NetworkTest.as: -------------------------------------------------------------------------------- 1 | #name "Network example" 2 | #author "Miss" 3 | #category "Examples" 4 | 5 | /* This plugin will open a TCP socket on port 80 to ip.mrag.nl, sends a 6 | * GET request to the server, and waits for a response to print to log. 7 | * 8 | * This is all done in an asynchronous manner - the connecting, the sending, 9 | * and the receiving of the response. This is accomplished by yielding. 10 | */ 11 | 12 | // Main() is our entry point. Note that when this function returns, the plugin 13 | // will be inactive. See also the Plugin_YieldTest example. 14 | void Main() 15 | { 16 | // Create a new socket. 17 | auto sock = Net::Socket(); 18 | 19 | // Try to initiate a socket to ip.mrag.nl on port 80. 20 | if (!sock.Connect("ip.mrag.nl", 80)) { 21 | // If it failed, there was some socket error. (This is not necessarily 22 | // a connection error!) 23 | print("Couldn't initiate socket connection."); 24 | return; 25 | } 26 | 27 | print("Connecting to host..."); 28 | 29 | // Wait until we are connected. This is indicated by whether we can write 30 | // to the socket. 31 | while (!sock.IsReady()) { 32 | yield(); 33 | } 34 | 35 | print("Connected! Sending request..."); 36 | 37 | // Send raw data (as a string) to the server. 38 | if (!sock.WriteRaw( 39 | "GET / HTTP/1.1\r\n" + 40 | "Host: ip.mrag.nl\r\n" + 41 | "User-agent: Openplanet NetworkTest Plugin\r\n" + 42 | "Connection: close\r\n" + 43 | "\r\n" 44 | )) { 45 | // If this fails, the socket might not be open. Something is wrong! 46 | print("Couldn't send data."); 47 | return; 48 | } 49 | 50 | print("Waiting for headers..."); 51 | 52 | // We are now ready to wait for the response. We'll need to note down 53 | // the content length from the response headers as well. 54 | int contentLength = 0; 55 | 56 | while (true) { 57 | // If there is no data available yet, yield and wait. 58 | while (sock.Available() == 0) { 59 | yield(); 60 | } 61 | 62 | // There's buffered data! Try to get a line from the buffer. 63 | string line; 64 | if (!sock.ReadLine(line)) { 65 | // We couldn't get a line at this point in time, so we'll wait a 66 | // bit longer. 67 | yield(); 68 | continue; 69 | } 70 | 71 | // We got a line! Trim it, since ReadLine() returns the line including 72 | // the newline characters. 73 | line = line.Trim(); 74 | 75 | // Parse the header line. 76 | auto parse = line.Split(":"); 77 | if (parse.Length == 2 && parse[0].ToLower() == "content-length") { 78 | // If this is the content length, remember it. 79 | contentLength = Text::ParseInt(parse[1].Trim()); 80 | } 81 | 82 | // If the line is empty, we are done reading all headers. 83 | if (line == "") { 84 | break; 85 | } 86 | 87 | // Print the header line. 88 | print("\"" + line + "\""); 89 | } 90 | 91 | print("Waiting for response..."); 92 | 93 | // At this point, we've parsed all the headers. We can now wait for the 94 | // actual response body. 95 | string response = ""; 96 | 97 | // While there is content to read from the body... 98 | while (contentLength > 0) { 99 | // Try to read up to contentLength. 100 | string chunk = sock.ReadRaw(contentLength); 101 | 102 | // Add the chunk to the response. 103 | response += chunk; 104 | 105 | // Subtract what we've read from the content length. 106 | contentLength -= chunk.Length; 107 | 108 | // If there's more to read, yield until the next frame. (Not necessary, 109 | // we could also only yield if there's no data available, but in this 110 | // example we don't care too much.) 111 | if (contentLength > 0) { 112 | yield(); 113 | } 114 | } 115 | 116 | // We're all done! 117 | print("All done!"); 118 | print("Response: \"" + response + "\""); 119 | 120 | // Close the socket. 121 | sock.Close(); 122 | } 123 | -------------------------------------------------------------------------------- /Plugin_SettingsAdvancedTest.as: -------------------------------------------------------------------------------- 1 | #name "Settings advanced example" 2 | #author "Phlarx" 3 | #category "Examples" 4 | 5 | /* This plugin demonstrates some techniques for storing 6 | * data with the Settings interface, when the data types 7 | * are not supported by the Setting annotation. 8 | * 9 | * In this demonstration, we will be storing vec3 objects. 10 | * This data type is supported by the Settings interface, 11 | * natively, but they will serve as a simple example that 12 | * can be extended to more complex use cases, and in fact, 13 | * these techniques are most useful when applied to objects 14 | * that are more expensive to construct. 15 | * 16 | * These examples are not intended to be the bible for 17 | * achieving these effects, but instead a set of ideas and 18 | * inspirations that you can apply to your use case. The 19 | * strategies used here can be mixed and matched, and built 20 | * upon. 21 | * 22 | * Finally, larger data structures may benefit more from 23 | * being stored separately in their own data file, instead 24 | * of being shoehorned into the Settings ini file. Take note 25 | * of this when deciding which strategy is best for your 26 | * specific application. 27 | */ 28 | 29 | /* Our goal for each vec3 is to approximate the following 30 | * code: 31 | * 32 | * [Setting] 33 | * vec3 Data; 34 | * 35 | * Remember, the above code is valid, but this file will 36 | * show techniques that are applicable to unsupported data 37 | * types. 38 | * 39 | * Each example has: 40 | * - a declaration, which defines the objects that we want 41 | * to store. 42 | * - getters and setters which are able to store the object 43 | * into the settings interface, and retrieve it again. 44 | * - accessors and mutators which are the script's normal 45 | * interactions with the objects. 46 | * 47 | * In addition, the third example, called Qux, has a 48 | * separate pair of functions for data conversion. The other 49 | * two examples do their data conversion at the same place as 50 | * one or more of the parts listed above. 51 | */ 52 | 53 | /* [[FooColor declaration]] 54 | * The first vec3, Foo, will be stored by creating separate 55 | * entries for each component. This allows the user to set 56 | * these values within the Settings dialog. Since the Setting 57 | * annotation doesn't provide the facilities to automatically 58 | * synchonize the vec3 directly, we'll need to use both 59 | * OnSettingsChanged and OnSettingsLoad to get the updates 60 | * via the Settings interface, as well as manually change the 61 | * component values when the vec3 is updated. 62 | * 63 | * Here, we see each float component declared individually, 64 | * with a Setting annotation for each, followed by the vec3 65 | * declaration. 66 | */ 67 | [Setting min=0.f max=1.f] 68 | float FooR = 1.f; 69 | [Setting min=0.f max=1.f] 70 | float FooG = 0.f; 71 | [Setting min=0.f max=1.f] 72 | float FooB = 0.f; 73 | vec3 FooColor = vec3(FooR, FooG, FooB); 74 | 75 | /* [[BarColor declaration]] 76 | * The second vec3, Bar, does not use separated component values, 77 | * and as such, it will not appear in the Settings dialog. As a 78 | * result, we need to only declare the actual vec3 object. 79 | */ 80 | vec3 BarColor = vec3(0.f, 1.f, 0.f); 81 | 82 | /* [[QuxColor declaration]] 83 | * The third vec3 is Qux, and like Bar, is does not appear in the 84 | * Settings dialog, and therefore we only need to declare the vec3. 85 | */ 86 | vec3 QuxColor = vec3(0.f, 0.f, 1.f); 87 | 88 | /* OnSettingsChanged is called whenever an update occurs within the 89 | * Settings dialog. 90 | */ 91 | void OnSettingsChanged() 92 | { 93 | /* [[FooColor get value from components]] 94 | * Since Foo is the only strategy which exposes the component 95 | * values to the Settings dialog, it is the only one which needs 96 | * to respond to that event. As the vec3 case is rather simple, 97 | * we can just immediately update the component values. 98 | */ 99 | FooColor.x = FooR; 100 | FooColor.y = FooG; 101 | FooColor.z = FooB; 102 | } 103 | 104 | /* OnSettingsSave is called when stopping a plugin, which may be 105 | * caused by, for example, reloading the plugin or exiting the 106 | * game. 107 | * 108 | * Note that the settings data is only written to the disk when 109 | * exiting the game, and is cached otherwise. 110 | */ 111 | void OnSettingsSave(Settings::Section& section) 112 | { 113 | /* [[BarColor set value to Settings]] 114 | * Here, we manually set the component values for Bar to its 115 | * component values. In the Settings ini file, this will look 116 | * almost identical to the approach for Foo, but by using this 117 | * route, the components do not appear within the settings menu, 118 | * and we don't need to worry about keeping the values 119 | * synchonized. 120 | * 121 | * As a side effect of doing this ourselves, values identical to 122 | * the default are not omitted from the ini, in contrast to Foo. 123 | */ 124 | section.SetFloat("BarR", BarColor.x); 125 | section.SetFloat("BarG", BarColor.y); 126 | section.SetFloat("BarB", BarColor.z); 127 | 128 | /* [[QuxColor set value to Settings]] 129 | * For Qux, we use a similar approach to Bar by setting the 130 | * Settings ini values directly, but in this case we've used a 131 | * conversion method to convert the value of Qux to some data 132 | * type matively handled by the settings interface. In this case, 133 | * we are using a string holding JSON data. The definition of 134 | * writeQux is found near the bottom of this file. 135 | */ 136 | section.SetString("Qux", writeQux(QuxColor)); 137 | } 138 | 139 | /* OnSettingsLoad is called when starting a plugin, which may be 140 | * caused by, for example, reloading the plugin or launching the 141 | * game. 142 | * 143 | * Note that the settings data is only read from the disk when 144 | * launching the game, and is cached otherwise. 145 | */ 146 | void OnSettingsLoad(Settings::Section& section) 147 | { 148 | /* [[FooColor get value from components]] 149 | * Since OnSettingsChanged is not triggered at plugin startup, 150 | * we replicate the actions for Foo here. 151 | */ 152 | FooColor.x = FooR; 153 | FooColor.y = FooG; 154 | FooColor.z = FooB; 155 | 156 | /* [[BarColor get value from Settings]] 157 | * When loading the values from the settings interface, we are 158 | * simply performing the inverse of what we had done in 159 | * OnSettingsSave. Additionally, we can provide default values 160 | * to use for each component, if that component is found to be 161 | * missing. 162 | */ 163 | BarColor.x = section.GetFloat("BarR", 0.f); 164 | BarColor.y = section.GetFloat("BarG", 1.f); 165 | BarColor.z = section.GetFloat("BarB", 0.f); 166 | 167 | /* [[QuxColor get value from Settings]] 168 | * In the case of Qux, the default value is the full json 169 | * description of the object, and the result of the Settings 170 | * load is passed through a converter to create the actual 171 | * object that we want. The definition of parseQux is found 172 | * near the bottom of this file. 173 | */ 174 | QuxColor = parseQux(section.GetString("Qux", "{'r':0,'g':0,'b':1}")); 175 | } 176 | 177 | void RenderInterface() 178 | { 179 | UI::Begin("Settings Advanced", UI::WindowFlags::AlwaysAutoResize); 180 | 181 | /* [[FooColor accesses and mutations]] 182 | * Our strategy for Foo shows real-time updates in the Settings 183 | * dialog, so we need to manually update the individual 184 | * components whenever Foo is updated. 185 | */ 186 | FooColor = UI::InputColor3("Foo Color", FooColor); 187 | FooR = FooColor.x; 188 | FooG = FooColor.y; 189 | FooB = FooColor.z; 190 | 191 | /* [[BarColor accesses and mutations]] 192 | * Since our strategy for Bar does not require updating the stored 193 | * values for use in the Settings dialog, we can use both direct 194 | * accesses and mutations. 195 | */ 196 | BarColor = UI::InputColor3("Bar Color", BarColor); 197 | 198 | /* [[QuxColor accesses and mutations]] 199 | * Like Bar, for Qux we can use both direct accesses and mutations. 200 | */ 201 | QuxColor = UI::InputColor3("Qux Color", QuxColor); 202 | 203 | UI::End(); 204 | } 205 | 206 | /* [[QuxColor datatype conversion]] 207 | * Where the component decomposition of Foo and Bar are scattered 208 | * in several placed throughout the source, the breakdown for Qux 209 | * is restricted to just the two conversion functions, parseQux and 210 | * writeQux. 211 | * 212 | * This particular implementation makes use of the Json group within 213 | * the Openplanet API, but a similar effect can be achieved with XML, 214 | * a bespoke serialization format, a unique identifier, or any of a 215 | * number of other things. 216 | 217 | * We need both directions for this conversion, and so writeQux is 218 | * the inverse operation of parseQux. To say it another way, 219 | * color == parseQux(writeQux(color)). 220 | */ 221 | vec3 parseQux(string json) 222 | { 223 | Json::Value obj = Json::Parse(json); 224 | vec3 color; 225 | color.x = obj.Get("r", 0.f); 226 | color.y = obj.Get("g", 0.f); 227 | color.z = obj.Get("b", 1.f); 228 | return color; 229 | } 230 | 231 | string writeQux(vec3 color) 232 | { 233 | Json::Value obj = Json::Object(); 234 | obj["r"] = Json::Value(color.x); 235 | obj["g"] = Json::Value(color.y); 236 | obj["b"] = Json::Value(color.z); 237 | return Json::Write(obj); 238 | } 239 | -------------------------------------------------------------------------------- /Plugin_SettingsTest.as: -------------------------------------------------------------------------------- 1 | #name "Settings example" 2 | #author "Miss" 3 | #category "Examples" 4 | 5 | /* This plugin demonstrates how persistent settings can be created and 6 | * automatically stored in Openplanet's Settings.ini file. 7 | * 8 | * Settings must be one of the following data types: 9 | * - bool 10 | * - int, float 11 | * - string 12 | * - enum 13 | * - vec2, vec3, vec4 14 | * More will be added in the future if they are deemed necessary. 15 | */ 16 | 17 | // Each setting can have a name and description, which will be displayed 18 | // in the plugin's settings panel. 19 | [Setting name="Print foo" description="Prints foo every second."] 20 | bool Foo = false; 21 | 22 | // If a setting also sets its initial value, it will be its default value. 23 | // Settings that have its default value will not be written to the config 24 | // file, meaning that if left unchanged, modifying them within the script 25 | // will always be applied. 26 | [Setting] 27 | int FooNumber = 10; 28 | 29 | // Integer settings can also have optional range values. If set, the settings 30 | // dialog will show a slider that travels between the specified range. Note 31 | // that the value can still go outside the range either by manually modifying 32 | // Settings.ini, or by Control-clicking the slider. 33 | [Setting min=-50 max=50] 34 | int FooNumberLimited; 35 | 36 | // Settings don't necessarily need to have a default value. In this case, 37 | // the default value is simply 0.0f. 38 | [Setting] 39 | float SomeFloat; 40 | 41 | // Float settings can also have range values. 42 | [Setting min=-2.5 max=2.5] 43 | float SomeFloatLimited; 44 | 45 | // This is a regular string setting. 46 | [Setting] 47 | string FooText = "Foo!"; 48 | 49 | // String settings can have a maximum length value. If set, the string can 50 | // not be made longer than the given amount of characters, unless changed 51 | // manually in Settings.ini. 52 | [Setting max=30] 53 | string FooTextLimited = "Foo..?"; 54 | 55 | // If a string setting is marked as multiline, it will appear as a multiline 56 | // editor field in the settings dialog. 57 | [Setting multiline] 58 | string FooTextMultiline = "Foo!\nThere's multiple lines here."; 59 | 60 | // For strings that represent passwords or other sensitive information, you 61 | // can mark it as a password field. This will mask the characters in the 62 | // settings dialog as asterisks. 63 | [Setting password] 64 | string FooTextPassword = "hunter2"; 65 | 66 | // Enum settings will appear as a drop-down selector in the settings dialog, 67 | // listing each defined enum value. 68 | [Setting] 69 | MyEnum FooEnum = MyEnum::Foo; 70 | 71 | // For vec3 and vec4 settings, the 'color' tag may be added. In the settings 72 | // dialog, a color selector will be shown for these values. 73 | [Setting color] 74 | vec4 FooColor = vec4(0, 0, 0, 0.7f); 75 | 76 | // All settings support the optional 'category' tag. If provided, settings 77 | // will be organized under tabs, for each category. If all settings are in 78 | // the same category, the tab bar will be hidden. If some settings have a 79 | // category and other do not (such as in this script), the settings without 80 | // a defined category will appear under the 'Uncategorized' tab. Tabs will 81 | // appear in the order that they are first encountered. 82 | [Setting category="Misc"] 83 | int FooCategorizedValue = 25; 84 | 85 | enum MyEnum 86 | { 87 | Foo, 88 | Bar, 89 | Baz 90 | } 91 | 92 | // Our main routine 93 | void Main() 94 | { 95 | while (true) { 96 | // We print some stuff based on our settings here. Note that they can 97 | // be changed on-the-fly, and changes made in the settings dialog are 98 | // immediately changed within the script. 99 | if (Foo) { 100 | print(FooText + " " + FooNumber); 101 | } 102 | sleep(1000); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Plugin_TableTest.as: -------------------------------------------------------------------------------- 1 | #name "Table example" 2 | #author "Miss" 3 | #category "Examples" 4 | 5 | /* This example shows how the tables UI works. It consists of the basics, which is simply drawing 6 | * the rows and columns, as well as sorting. 7 | */ 8 | 9 | class Item 10 | { 11 | int m_a; 12 | int m_b; 13 | float m_c; 14 | float m_d; 15 | } 16 | 17 | array g_items; 18 | 19 | // This bool exists for 2 reasons: 20 | // 1. So columns can be sorted when the list of items changed 21 | // 2. So columns can be sorted after a plugin reload (first rendered frame) 22 | bool g_itemsDirty; 23 | 24 | void AddNewItem() 25 | { 26 | auto newItem = Item(); 27 | newItem.m_a = Math::Rand(0, 1000000); 28 | newItem.m_b = Math::Rand(0, 999999); 29 | newItem.m_c = Math::Rand(0.0f, 100.0f); 30 | newItem.m_d = Math::Rand(100.0f, 500.0f); 31 | g_items.InsertLast(newItem); 32 | g_itemsDirty = true; 33 | } 34 | 35 | void ClearItems() 36 | { 37 | g_items.RemoveRange(0, g_items.Length); 38 | g_itemsDirty = true; 39 | } 40 | 41 | void RandomItems() 42 | { 43 | ClearItems(); 44 | 45 | int numItems = Math::Rand(5, 25); 46 | for (int i = 0; i < numItems; i++) { 47 | AddNewItem(); 48 | } 49 | } 50 | 51 | void SortItems(UI::TableSortSpecs@ sortSpecs) 52 | { 53 | // Trying to sort an array of less than 2 items will throw an index out of bounds exception 54 | if (g_items.Length < 2) { 55 | return; 56 | } 57 | 58 | auto specs = sortSpecs.Specs; 59 | for (uint i = 0; i < specs.Length; i++) { 60 | auto spec = specs[i]; 61 | 62 | // If there is no direction specified, we just skip it 63 | if (spec.SortDirection == UI::SortDirection::None) { 64 | continue; 65 | } 66 | 67 | // Sort based on direction 68 | if (spec.SortDirection == UI::SortDirection::Ascending) { 69 | // Sort ascending based on column index 70 | switch (spec.ColumnIndex) { 71 | case 0: g_items.Sort(function(a, b) { return a.m_a < b.m_a; }); break; 72 | case 1: g_items.Sort(function(a, b) { return a.m_b < b.m_b; }); break; 73 | case 2: g_items.Sort(function(a, b) { return a.m_c < b.m_c; }); break; 74 | case 3: g_items.Sort(function(a, b) { return a.m_d < b.m_d; }); break; 75 | } 76 | 77 | } else if (spec.SortDirection == UI::SortDirection::Descending) { 78 | // Sort descending based on column index 79 | switch (spec.ColumnIndex) { 80 | case 0: g_items.Sort(function(a, b) { return a.m_a > b.m_a; }); break; 81 | case 1: g_items.Sort(function(a, b) { return a.m_b > b.m_b; }); break; 82 | case 2: g_items.Sort(function(a, b) { return a.m_c > b.m_c; }); break; 83 | case 3: g_items.Sort(function(a, b) { return a.m_d > b.m_d; }); break; 84 | } 85 | } 86 | } 87 | 88 | // Clear dirty flags 89 | sortSpecs.Dirty = false; 90 | g_itemsDirty = false; 91 | } 92 | 93 | void RenderInterface() 94 | { 95 | if (UI::Begin("Table test", UI::WindowFlags::NoCollapse | UI::WindowFlags::MenuBar)) { 96 | if (UI::BeginMenuBar()) { 97 | if (UI::MenuItem("Add item")) { AddNewItem(); } 98 | if (UI::MenuItem("Clear items")) { ClearItems(); } 99 | if (UI::MenuItem("Randomize items")) { RandomItems(); } 100 | UI::EndMenuBar(); 101 | } 102 | 103 | // Make sure the UI::TableFlags::Sortable flag is used here 104 | if (UI::BeginTable("Items", 4, UI::TableFlags::Sortable | UI::TableFlags::Resizable)) { 105 | // Set up table columns 106 | UI::TableSetupColumn("A", UI::TableColumnFlags::WidthFixed, 100); 107 | UI::TableSetupColumn("B", UI::TableColumnFlags::WidthFixed | UI::TableColumnFlags::DefaultSort | UI::TableColumnFlags::PreferSortDescending, 100); 108 | UI::TableSetupColumn("C", UI::TableColumnFlags::WidthStretch); 109 | UI::TableSetupColumn("D", UI::TableColumnFlags::WidthFixed, 100); 110 | UI::TableHeadersRow(); 111 | 112 | // Perform the actual sorting here when the sorting specs are marked as dirty (user changed 113 | // table sorting) or the items array is marked as dirty (new or removed items) 114 | auto sortSpecs = UI::TableGetSortSpecs(); 115 | if (sortSpecs !is null && (sortSpecs.Dirty || g_itemsDirty)) { 116 | SortItems(sortSpecs); 117 | } 118 | 119 | // Draw the items 120 | for (uint i = 0; i < g_items.Length; i++) { 121 | auto item = g_items[i]; 122 | UI::TableNextRow(); 123 | UI::TableSetColumnIndex(0); 124 | UI::Text("" + item.m_a); 125 | UI::TableSetColumnIndex(1); 126 | UI::Text("\\$f77" + item.m_b); 127 | UI::TableSetColumnIndex(2); 128 | UI::Text("\\$7f7" + item.m_c); 129 | UI::TableSetColumnIndex(3); 130 | UI::Text("\\$ff7" + item.m_d); 131 | } 132 | 133 | UI::EndTable(); 134 | } 135 | } 136 | UI::End(); 137 | } 138 | 139 | void Main() 140 | { 141 | RandomItems(); 142 | } 143 | -------------------------------------------------------------------------------- /Plugin_YieldTest.as: -------------------------------------------------------------------------------- 1 | #name "Yield example" 2 | #author "Miss" 3 | #category "Examples" 4 | 5 | /* This plugin demonstrates how a script loop works. Openplanet will 6 | * execute the Main() function in our plugin every game/server frame. 7 | * 8 | * We can then call `yield()` to suspend script execution and return 9 | * execution back to Openplanet & Maniaplanet, or we can call `sleep()` 10 | * with a milliseconds parameter to yield for a specific amount of time. 11 | * 12 | * In this example, we will increase a variable by 2 on the first frame, 13 | * decrease it by 1 the second frame, then wait 1 second and do it again. 14 | * 15 | * Note that we can only yield if we're inside of a coroutine. Main() is 16 | * in fact a coroutine itself, so we may use it in there. Also see 17 | * Plugin_CoroutineTest.as for more details. 18 | */ 19 | 20 | // This is the variable we will be modifying in our script loop. 21 | int g_num = 0; 22 | 23 | // Main() is our entry point function for our script loop. 24 | void Main() 25 | { 26 | // This will be our script loop. 27 | while (true) { 28 | // Add 2. 29 | g_num += 2; 30 | print(Time::Now + " Num = " + g_num); 31 | 32 | // Suspend script execution. 33 | yield(); 34 | 35 | // When script execution is resumed in the next frame, we continue 36 | // from here. So let's subtract 1 in this frame. 37 | g_num -= 1; 38 | print(Time::Now + " Num = " + g_num); 39 | 40 | // Let's suspend the script for 1 second. 41 | sleep(1000); 42 | } 43 | 44 | // The plugin lifetime is over when the end of this function is reached. 45 | } 46 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Example Scripts 2 | These are example scripts for Openplanet. All examples are fully commented to explain what's going 3 | on in the code. 4 | 5 | If you have a script you want to contribute to this repository, please don't hesitate [submitting a 6 | pull request](/openplanet-nl/example-scripts/compare). Note that any plugins you contribute must be 7 | fully commented and easy to follow before I will accept them to this repository. 8 | 9 | ## License 10 | All scripts in this repository are in the public domain. You can use them however you wish. 11 | --------------------------------------------------------------------------------