/sFramework

sFramework is a set of tools and utilities mainly used by the sUDE mods; it could be considered the core of the sUDE project

Primary LanguageCMIT LicenseMIT

sFramework



Getting started

sFramework is the core of the sUDE project.

It ships many features and utilities used and/or implemented by sUDE modules :





PostProcessing effects

sFramework ships a centralized post processing effects manager, with the goal of allowing multiple requests of the same effects, without hardcoding them.


SPPEffect

SPPEffect is the "container" of any PostProcess Effect you wish to add to it (e.g. saturation, vignette, motion blur etc.).

SPPEffect myPPE = new SPPEffect();

To add a parameter use the provided setters:

myPPE.setVignette(intensity, color);
myPPE.setRadialBlur(powerX, powerY, offsetX, offsetY);
myPPE.setChromAber(powerX, powerY);
//...

To apply it, "hand it over" to the SPPEManager, which will calculate the correct value of all active SPPEffect and then apply it

SPPEManager.activate(myPPE);

and to deactivate it:

SPPEManager.deactivate(myPPE);

SPPEffectAnimated

A SPPEffectAnimated is just like a SPPEffect, but it has an animation mechanism which allows you to animate the values of a PostProcess effect.

A SPPEffectAnimated is an abstract class. You need to implement it with your own class and override the onAnimate() method, which will be called on every frame.

There also is a timed variant SPPEffectTimed, which will be automatically deactivated once a certain amount has passed.

To create your animation, simply extend either SPPEffectAnimated or SPPEffectTimed

class MyLoopAnimation : PPELoopedParams{
    override void onAnimate(float deltaTime){
        /* change PPE values here
        setOverlay(...);
        setChromAber(...);
        setCameraEffects(...);
        */
        setVignetteIntensity( Math.Sin(getTime()) );
    }
}

class MyTimedAnimation : SPPEffectTimed{
    override void onAnimate(float deltaTime){
        setVignetteIntensity( Math.Cos(getTime()) );
    }
}

A SPPEffectTimed also has a "duration" which can be set with the constructor, or the provided method:

MyTimedAnimation myTimedAnimation = new MyTimedAnimation(6); // the animation will last 6 seconds
myTimedAnimation.setDuration(10.0); // the animation will last 10 seconds

The activation of the animation is identical to any other SPPEffect

MyLoopAnimation myAnimation = new MyLoopAnimation();
SPPEManager.activate(myAnimation);

MyTimedAnimation myTimedAnimation = new MyTimedAnimation(5.5);
SPPEManager.activate(myTimedAnimation);

If you want to manually manage the animation you can use the provided methods

myAnimation.start();  // Set the animation state to "Playing"
myAnimation.stop();   // Reset the time and set the animation state to "Stopped"
myAnimation.pause();  // Freeze the animation values and set the animation state to "Paused"
myAnimation.resume(); // Resume the the animation and set the animation state to "Playing"

The insides of SPPEManager

PostProcess Effect Manager

The SPPEManager is in charge of managing the PostProcessing effects; this is a small diagram roughly showing how it works

--- ---



Camera Overlays

A camera overlay is nothing else than an image, used like an HUD. The fundemental unit of camera overlays is the SCameraOverlay, a very simple wrapper for the ImageWidget (the DayZ UI component that holds an image).

It can be used in countless ways:

As an animated UI :

or for emulating headgear damage:

(from sVisual, MotoHelmet in various health state: Pristine, Worn, Damaged, BadlyDamaged and Ruined)


Defining an overlay is very simple and very similar to SPPEffects, in fact there are three types as well and the logic is identical to the SPPEffects:

  • SCameraOverlay
  • SCameraOverlayAnimated
  • SCameraOverlayTimed
class MyAnimatedOverlay : SCameraOverlayAnimated {

    override void onInit(){
        setImage("path/to/texture.edds");
        //...
    }

    //onAnimate() gets called every frame!
    override void onAnimate(float deltaTime){
        setSize(Math.Sin(getTime()));
        //setPosition(...)
        //setRotation(...)
        //setMask(...)
        //...
    }
}

To activate/deactivate an overlay, you use the SCameraOverlayManager:

// NOTE: SCameraOverlaysManager is a singleton
SCameraOverlaysManager.getInstance().activate(myOverlay);

Clothing overlays

sFramework is capable of automatically activating/deactivating overlays when a clothing item is equipped/unequipped; making use of this feature is super easy. You just need to define a list of overlays inside your clothing item in your config.cpp as follows:

class YOUR_CLOTHING_ITEM_CLASSNAME{
    class sUDE {
        class CameraOverlays {
            class overlay_0 : SCameraOverlay {
                image="path/to/your/image/pristine.edds";
            };
            class overlay_1 : SCameraOverlay {
                image="path/to/your/image/worn.edds";
            };
            class overlay_2 : SCameraOverlay {
                image="path/to/your/image/damaged.edds";
            };
            class overlay_3 : SCameraOverlay {
                image="path/to/your/image/badlydamaged.edds";
            };
            /*
            class overlay_X : SCameraOverlay {
                image="path/to/your/image/xxx.edds";
            };
            */
        };
    };
};

A SCameraOverlay has many attributes you can play with, which can be set either by scripts or in the config. Currently available attributes are:

image="";                                 // Resource image path, can be whatever an ImageWidget accepts texture
alpha=1.0;                                // [0.0 - 1.0] Alpha value (transparency)
mask="";                                  // Resource image path, can be whatever an ImageWidget accepts as mask
maskProgress=1.0;                         // [0.0 - 1.0] Mask progress
maskTransitionWidth=1.0;                  // Mask transition width (used as progress + transitionWidth)
position[] = {0.0, 0.0};                  // [0.0 - 1.0] X and Y position in screenspace
size[] = {1.0, 1.0};                      // [0.0 - 1.0] X and Y size in screenspace
rotation[] = {0.0, 0.0, 0.0};             // Yaw, Pitch and Roll defined in degrees
priority = 0;                             // Higher priority means closer to the camera (also known as z-depth)
targetCameras[] = {"DayZPlayerCamera"};   // Camera typename on which the overlay will be visible
hidesWithIngameHUD = 0;                   // [0 = false, 1 = true] Determines if it must hides when the player hides the ingame HUD


Configurations interfaces

SUserConfig

SUserConfig has the purpose to help in creating user (client) settings in just few lines of code.

Implement the SUserConfigBase as follows:

class MySUserConfig : SUserConfigBase {

    /**
    *   Where the config will be saved
    */
    override string getPath(){
        return "$saves:\\path\\to\\my\\config.json";
    }
    
    /**
    *   Where the config with default values will be saved
    */
    override string getDefaultPath(){
        return "$profile:\\path\\to\\my\\config_default.json";
    }

    /**
    *   Implement the deserialization
    */
    override void deserialize(string data, out string error){
        auto cfg = this;
        m_serializer.ReadFromString(cfg, data, error);
    }
    
    /**
    *   Implement the serialization
    */
    override string serialize() {
        string result;
        auto cfg = this;
        getSerializer().WriteToString(cfg, true, result);
        return result;
    }
    
    override string serializeDefault() {
        string result;
        auto cfg = new MySUserConfig();
        getSerializer().WriteToString(cfg, true, result);
        return result;
    }

    // Configuration options (and their default values) you want to store
    float myFloatOption = 0.69;
    //bool myBoolOption = true;
    //int myIntOption = 69;
    //ref array<float> myarrayOption = {0.69, 42.0, 420.69, 0.42069};
    //any other options 

    override void registerOptions() {
        super.registerOptions();
        registerOption("myFloatOption",     new SUserConfigOption<float>(myFloatOption));
        // registerOption("myBoolOption",   new SUserConfigOption<bool>(myBoolOption));
        // registerOption("myIntOption",    new SUserConfigOption<int>(myIntOption));
        // registerOption("myarrayOption",  new SUserConfigOptionArray<float>(myarrayOption));
        // any other option
    }
}

you can now save it, load it and more with few lines

MyUserConfig myCfg = new MyUserConfig();
myCfg.load();
myCfg.save();
//myCfg.isValid()
// etc.

Making a user interface for changing those option is very easy too.

class MyOptionsMenu : SOptionsMenuBase {
    
    override string getName() {
        return "MyOptionsName";
    }
    
    override string getLayout() {
        return "path/to/interface.layout";	
    }
        
    ref SliderWidget    myFloatOptionSlider;
    ref CheckBoxWidget  myBoolOptionCheckbox;
    
    override void onInit() {
        super.onInit();
        setUserConfig(instanceOfYourConfig());
    }
    
    override void onBuild() {
        super.onBuild();
        //               Widget to link          name of widget in your layout      option to link
        initOptionWidget(myFloatOptionSlider,    "myFloatOption",                    getUserConfig().getOptionFloat("myFloatOption"));
        initOptionWidget(myBoolOptionCheckbox,   "myBoolOption",                     getUserConfig().getOptionBool("myBoolOption"));
    }
    
}

SGameConfig

SGameConfig contains just a set of utilities to read the game config.cpp more easily



sTest (UnitTesting framework)

sFramework also ships sTest, a UnitTesting framework for Enforce scripts, based on industry standard frameworks such as JUnit for Java.

Test units are super simple to define and use:

  1. Create a TestUnit class (extends STestUnit)
  2. Create some test cases function
  3. Register the test cases by passing the test case name (function name) to registerTestCases method
class MyTestUnit : STestUnit {
    
    override void init() {
        registerTestCases({
            "testThisFeature",
            "testThisOtherFeature",
            "shouldFail"
        });
    }

    void testThisFeature() {
        // do something...
        // assert something
        assertEqual(10, 5 + 5);
    }

    void testThisOtherFeature() {
        // do something...
        // assert something
        assertTrue(true);
    }

    void shouldFail() {
        // this test case will fail!
        Class someClass = null;
        assertNotNull(someClass);
    }
}

You can now run the test unit by passing its name to sTest:

TIP: you can execute the following in the workbench console

STest.run(MyTestUnit);

// optionally an array of test units can be used to run multiple test units
STest.run({MyTestUnit, MyOtherTestUnit, MyLastTestUnit});

The result of the tests can be seen in the output window of the workbench or inside sUDE logs.

=======================================================================
Running tests...
-----------------------------------------------------------------------
MyTestUnit
│    ├ [ ✓ ] PASSED  - testThisFeature
│    ├ [ ✓ ] PASSED  - testThisOtherFeature
│    ├ [ × ] FAILED  - shouldFail
│    │    ├ Expected: true
│    │    ├ Actual:   false
-----------------------------------------------------------------------
                   PASSED    |    FAILED    |    SKIPPED
                     2              1               0
=======================================================================

You can decide not to stop when a test fails:

STest.shouldContinueAtFail = true; // default: false

or to change verbosity in logging:

STest.verbosity = 3; // default: 1

Assertions

You have access to multiple assertions:

  • assertEqual(x, y) with x and y of type float, int, string, bool, array<float>
  • assertTrue(x) with x of type bool
  • assertFalse(x) with x of type bool
  • assertNull(x) with x of type Class
  • assertNotNull(x) with x of type Class

Advanced TestUnit usage

If you need to perform some actions before or after each test unit or test case you can define and register some callbacks:

class MyTestUnit : STestUnit {
    
    override void init() {

        registerBeforeClassCallbacks({
            "doSomethingBeforeTestUnit"
        });
        
        registerBeforeCallbacks({
            "doSomethingBeforeEachTestCase"
        });

        registerAfterCallbacks({
            "doSomethingAfterEachTestCase"
        });

        registerAfterClassCallbacks({
            "doSomethingAfterTestUnit"
        });

        // registerTestCases({
        //     ...
        // });
    }

    void doSomethingBeforeTestUnit() {
        // do something ...
    }

    void doSomethingBeforeEachTestCase() {
        // do something ...
    }

    void doSomethingAfterEachTestCase() {
        // do something ...
    }

    void doSomethingAfterTestUnit() {
        // do something ...
    }

}

If you need to write some more complext test cases, you can also manually fail(), pass() or skip(). Example:

void testSomethingComplex() {
    int x = 2;
    int y = 2;
    int actual = x + y;
    int expected = 4;

    if ( x == y) {
        fail("x and y not equal", "x and y are equal", "Failed during X and Y comparison");
    } else {
        assertEqual(expected, actual);
    }
}


Utilities

SDebugUI

A fully featured API for quick creation of debug intefaces.

class SomeClass {
    void OnUpdate(float timeslice) {
        auto dui = SDebugUI.of("TestDebugUI");
        dui.begin();
        dui.window("Debug monitor");
            dui.text("Day Time : " + GetGame().GetDayTime());
            dui.newline();
            
            dui.textrich("<image set='dayz_gui' name='icon_pin' /> ");
            dui.textrich("You can click on the slider, or you can use the mouse wheel");
            dui.textrich("If you hold shift while using mouse wheel, it will go wrooom!");
            float sliderValue;
            dui.slider("mySlider", sliderValue);
            dui.textrich("The value of <font name='gui/fonts/amorserifpro'>sliderValue</font> is: <b>"+ sliderValue +"</b>");
            
            bool checkValue
            dui.check("myCheck", checkValue);
            dui.text("CheckValue: " + checkValue);
            dui.button("click me", this, "printSum", new Param2<int,int>(69, 420));
            dui.newline();
            dui.table({
                {"Attribute",    "Value"},
                {"Time",         ""+GetGame().GetTickTime()},
                {"Radio volume", ""+GetGame().GetSoundScene().GetRadioVolume()},
                {"VoIP volume",  ""+GetGame().GetSoundScene().GetVOIPVolume()},
                {"VoIP level",   ""+GetGame().GetSoundScene().GetAudioLevel()}
            });
            dui.plotlive("Sin", Easing.EaseInBounce(Math.AbsFloat(Math.Sin(m_time))));
        dui.end();
    }

    void printSum(int x, int y) {
        Print(x + y);
    }
}

SColor

SColor helps you defining and using colors. A few examples:

//hex values, like in CSS
SColor.rgb(0xFF0000);    //red
SColor.rgba(0xFF000055); //red slightly transparent
SColor.argb(0x55FF0000); //red slightly transparent

//separated rgb channels
SColor.rgb(60, 97, 178);      //blueish
SColor.rgba(60, 97, 178, 0);  //blueish
SColor.argb(0, 60, 97, 178);  //blueish

// hue saturation and brightness
SColor.hsb(0.60, 0.65, 0.87); //yellowish

// presets (taken from https://www.w3schools.com/cssref/css_colors.asp)
SColor.rgb(RGBColors.RED);
SColor.rgb(RGBColors.AQUAMARINE);
SColor.rgb(RGBColors.YELLOW_GREEN);

SObservableArray

A list that allows listeners to track changes when they occur.

class MyClass {
    
    ref SObservableArray<int> observableArray = new SObservableArray<int>();

    void MyClass() {
        observableArray.addOnChangeListener(new SArrayChangeListener(this, "onChange"));

        // multiple listeners can be added
        observableArray.addOnInsertListener(new SArrayInsertListener(this, "onInsert"));
        observableArray.addOnInsertListener(new SArrayInsertListener(this, "onInsert2"));

        observableArray.addOnPreRemoveListener(new SArrayPreRemoveListener(this, "onPreRemove"));
        observableArray.addOnClearListener(new SArrayClearListener(this, "onClear"));
    }

    void onChange() {
        SLog.d("Array has changed");
    }

    void onInsert(int value, int position) {
        SLog.d("Value " + value + " has been inserted in position " + position);
    }

    void onInsert2(int value, int position) {
        // do somehting... 
    }

    void onPreRemove(int indexToBeRemoved) {
        SLog.d("Index " + indexToBeRemoved + " will be removed");
    }

    void onClear() {
        SLog.d("Array has been cleared");
    }

}
observableArray.insert(69); // onChange, onInsert and onInsert2 will be called
observableArray.insert(420); // onChange, onInsert and onInsert2 will be called

observableArray.removeItem(69); // onPreRemove and onChange will be called
observableArray.remove(0); // onPreRemove and onChange will be called
observableArray.clear(); // onClear and onChange will be called

SSpawnable

SSpawnable helps you in quickly spawn items with a lot of attachments:

// Build an M4A1 with multiple attachments
SSpawnable m4 = SSpawnable.build("M4A1").withAttachments({
    "M4_Suppressor",
    "M4_OEBttstck",
    "M4_RISHndgrd"
});

// Build an M16A2 with no attachments
SSpawnable m16 = SSpawnable.build("M16A2");

// Build an AK101 with multiple attachments (and they attachments too)
SSpawnable ak = SSpawnable.build("AK101").withAttachments({
    "AK_Suppressor",
    "AK_PlasticBttstck",
    "AK_RailHndgrd"
}).withSpawnableAttachments(
    (new SSpawnable("PSO11Optic")).withAttachment("Battery9V"),
    (new SSpawnable("UniversalLight")).withAttachment("Battery9V"));

// Actually spawn the items
m4.spawn(position);
m16.spawn(position);
ak.spawn(position);

SRaycast

SRaycast helps you launching raycasts with more flexibility:

SRaycast ray = new SRaycast(/**...*/);
vector contactPositon = ray
    .from(thisPosition)
    .to(thisOtherPosition)
    .ignore(thisItem, thisOtherItem)
    .launch()
    .getContactPosition();

if (ray.hasHit()){
    SLog.d("Raycast has hit at this position" + contactPositon);
}

SFlagOperator

SFlagOperator helps you in bitwise operations, especially when working with flags, hence the name.

enum MyFlags {
    A = 1,
    B = 2,
    C = 4,
    D = 8,
    E = 16
}

SFlagOperator fop = new SFlagOperator(MyFlags.A | MyFlags.C);
SLog.d("Result : " + fop.collectBinaryString());
// Result : 0000 00101

fop.set(MyFlags.B);
fop.reset(MyFlags.A)
SLog.d("Result : " + fop.collectBinaryString());
// Result : 0000 00110

SLog.d("A is set : " + fop.check(MyFlags.A));
//A is set : false

SLog.d("B is set : " + fop.check(MyFlags.B));
//B is set : true


Contact me

Found a bug or want to give a suggestion? Feel free to contact me!





Buy me a coffee :)