Magic Light v1.3

This bundle is marked as approved. It works and satisfies the submission rules.
  • Like
Reactions: xAerox
Spell Code
JASS:
//***************************************************************************
//***************************************************************************
//***************************************************************************
//      M A G I C    L I G H T
//                                          By: Elphis (Nyuu)
//      Version: 1.3
//
//      Sepll Description: 
//  `                       - Create a light swirling around the caster, then it will fly around, 
//                           creating a protective ring of light, when the light hit allies will recover 
//                           10/20/30/40 / light (Maximum: 150/300/450/600) hit points, 
//                           when the light hit enemies will cause damage by 20/40/60/80 / light (Maximum: 300/600/900/1200).

//     Credits:
//              GreenLight Model - http://www.hiveworkshop.com/forums/models-530/green-light-243630/
//              BoundSentinel - http://www.wc3c.net/showthread.php?t=102576

//          - Installation:
//                                - Import/copy Magic Light code and Require folder to your map
//                                - Import/copy the custom ability and unit to your map and change the SPELL_ID, DUMMY_LIGHT if needed
//                                - You may view the raw ID of the objects by pressing CTRL+D in the object editor
//                                - You may play with the configurables below
//
//
//***************************************************************************
//***************************************************************************
//***************************************************************************

library MagicLight
    
    globals
        //Spell rawcode, change if needed
        private constant integer SPELL_ID = 'A000'
        //Dummy rawcode, change if needed
        private constant integer DUMMY_ID = 'e000'
        //Dummy count per waves, change if needed
        private constant integer LIGHT_COUNT = 15
        //Total waves of the lights
        private constant integer LIGHT_WAVE = 3
        //Lights create per peroid
        private constant real LIGHT_TIK = 1.
        //Timer period
        private constant real PERIODIC = .031250000
        //
        private constant real LIGHT_SPEED = 5.
        private constant real LIGHT_CIRCLE_SPEED = 5.
        private constant real START_DISTANCE = 100.
        //Lights scale
        private constant real LIGHT_SCALE = 3.
        //Damages base when it hit enemies
        private constant real DAMAGE_BASE = 20.
        //Heals base when it hit allies
        private constant real HEAL_BASE = 10.
        //Value betweens lights and allies/enemies heals/damages radius
        private constant real LIGHT_AOE = 150.
        //Full area of effects of this spell
        private constant real MAX_AOE = 600.
        //**************************************************************
        private constant attacktype AT = ATTACK_TYPE_HERO
        private constant damagetype DT = DAMAGE_TYPE_DEATH
        private constant weapontype WT = WEAPON_TYPE_CLAW_HEAVY_SLICE
        //**************************************************************
        //Heals effect when the lights hit allies
        private constant string HEAL_EFFECT = "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl"
        //Damages effect when the lights hit allies
        private constant string DAMAGE_EFFECT = "war3mapImported\\Green Light.mdx"
        //Disappear effect when the light reach maximum area of effects
        private constant string LIGHT_EXPIRE = "Abilities\\Spells\\Human\\DispelMagic\\DispelMagicTarget.mdl"
        //Damage enemies effect attachment
        private constant string DAMAGE_ATTACH = "origin"
        //Heal allies effect attachment
        private constant string HEAL_ATTACH = "origin"
        //Dummy owner
        private constant player DUMMY_OWNER = Player(15)
        //Player that filterFunction will be skipped
        private constant player SKIP_PLAYER = Player(15)
        //**************************************************************
        //          NON - CONFIGURATION
        //**************************************************************
        private constant timer TIMER = CreateTimer()
        private constant timer TIMER_2 = CreateTimer()
        
        private constant group G = CreateGroup()
        
        private          integer M = -1
        private          integer array SM 
        private          integer PM = -1
        private          integer array PSM
        //**************************************************************
    endglobals
    
    //Damage settings*****************************************************
    private constant function getDamage takes unit u,integer lvl returns real
        return DAMAGE_BASE*lvl
    endfunction
    //******************************************************************
    //Heal settings*****************************************************
    private constant function Heal takes unit u,integer lvl returns real
        return HEAL_BASE*lvl
    endfunction
    //********************************************************************
    //Filter Function Settings******************************************
    private constant function filterFunc takes unit filterUnit returns boolean
        return not IsUnitType(filterUnit,UNIT_TYPE_DEAD) and GetOwningPlayer(filterUnit) != SKIP_PLAYER and GetUnitTypeId(filterUnit) != 0
    endfunction
    //********************************************************************
    
    private struct MagicLight
        
        unit caster = null
        
        integer lightwave = LIGHT_WAVE
        
        real tik = 0.
        
        static constant real inBetween = 360./LIGHT_COUNT*.0174533
        
        static method onPeriodic takes nothing returns nothing
            local integer i = 0
            local integer l
            
            local real x
            local real y
            local real ux
            local real uy
            local real d
            
            local unit u = null
            
            local thistype this
            
            loop
                exitwhen i > M
                
                set this = SM[i]
                
                if tik < LIGHT_TIK then
                    set tik = tik + PERIODIC
                else
                    
                    set tik = 0.
                    
                    if lightwave > 0 then
                        
                        set l = 1
                        
                        set ux = GetUnitX(caster)
                        set uy = GetUnitY(caster)
                        
                        loop
                            exitwhen l > LIGHT_COUNT
                            
                            set d = (l*inBetween)
                            
                            set x = ux + START_DISTANCE * Cos(d)
                            set y = uy + START_DISTANCE * Sin(d)
                            
                            set u = CreateUnit(DUMMY_OWNER,DUMMY_ID,x,y,0.)
                            
                            call SetUnitScale(u,LIGHT_SCALE,0.,0.)
                            
                            set d = bj_RADTODEG*Atan2(y-uy,x-ux)
                            
                            call MagicLight_MagicLightPlugin.add(u,caster,d)
                            
                            set l = l + 1
                        endloop
                        
                        set lightwave = lightwave - 1
                        
                    else
                    
                        set caster = null
                        
                        set SM[i] = SM[M]
                        set SM[M] = -2
                        set M = M - 1
                        
                        call destroy()
                        
                        if M == -1 then
                            call PauseTimer(TIMER)
                        endif
                    
                    endif
                
                    set lightwave = lightwave - 1
                    
                    set u = null
                    
                endif
                
                set i = i + 1
            endloop
        endmethod
        
        static method onCast takes nothing returns boolean
            local thistype this
            
            local integer l
            
            local real x
            local real y
            local real d
            local real ux
            local real uy
            
            local unit u
            
            if GetSpellAbilityId() == SPELL_ID then
                set this = allocate()
                
                set M = M + 1
                set SM[M] = this
                
                set caster = GetTriggerUnit()
                
                set l = 1
                
                set ux = GetUnitX(caster)
                set uy = GetUnitY(caster)
                
                loop
                    exitwhen l > LIGHT_COUNT
                    
                    set d = (l*inBetween)
                    
                    set x = ux + START_DISTANCE * Cos(d)
                    set y = uy + START_DISTANCE * Sin(d)
                    
                    set u = CreateUnit(DUMMY_OWNER,DUMMY_ID,x,y,0.)
                    
                    call SetUnitScale(u,LIGHT_SCALE,0.,0.)
                    
                    set d = bj_RADTODEG*Atan2(y-GetUnitY(caster),x-GetUnitX(caster))
                    
                    call MagicLight_MagicLightPlugin.add(u,caster,d)
                    
                    set l = l + 1
                endloop
                
                set u = null
                
                if M == 0 then
                    call TimerStart(TIMER,PERIODIC,true,function thistype.onPeriodic)
                endif
            endif
            
            return false
        endmethod
        
        static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            local integer i = 0
            
            loop
                exitwhen i > 15
                
                call TriggerRegisterPlayerUnitEvent(t,Player(i),EVENT_PLAYER_UNIT_SPELL_EFFECT,null)
                
                set i = i + 1
            endloop
            
            call TriggerAddCondition(t,function thistype.onCast)
        endmethod
        
    endstruct
    
    
    public struct MagicLightPlugin
        
        unit light
        unit caster
        
        player p
        
        real angle
        real distance = START_DISTANCE
        real dmg
        real heal
        
        static method onPeriodic takes nothing returns nothing
            local integer i = 0
            local thistype this
            
            local real x
            local real y
            local real a
            
            local unit f = null
            
            loop
                exitwhen i > PM
                
                set this = PSM[i]
                
                if distance < MAX_AOE then
                
                    set distance = distance + LIGHT_CIRCLE_SPEED
                    set angle = angle + LIGHT_SPEED
                    set a = .0174533*angle
                    
                    set x = GetUnitX(caster) + distance * Cos(a)
                    set y = GetUnitY(caster) + distance * Sin(a)
                    
                    call SetUnitX(light,x)
                    call SetUnitY(light,y)
                    
                    call GroupEnumUnitsInRange(G,x,y,LIGHT_AOE,null)
                    
                    loop
                        set f = FirstOfGroup(G)
                        exitwhen f == null
                        
                        if filterFunc(f) and f != caster then
                        
                            if IsUnitEnemy(f,p) then
                                call UnitDamageTarget(caster,f,dmg,true,false,AT,DT,WT)
                                call DestroyEffect(AddSpecialEffectTarget(DAMAGE_EFFECT,f,DAMAGE_ATTACH))
                            else
                                call SetWidgetLife(f,GetWidgetLife(f)+heal)
                                call DestroyEffect(AddSpecialEffectTarget(HEAL_EFFECT,f,HEAL_ATTACH))
                            endif
                            
                            set distance = 99999.
                            
                        endif
                        
                        call GroupRemoveUnit(G,f)
                    endloop
                else
                    
                    if distance != 99999. then
                        call DestroyEffect(AddSpecialEffect(LIGHT_EXPIRE,GetUnitX(light),GetUnitY(light)))
                    endif
                    
                    call RemoveUnit(light)
                            
                    set light = null
                    set caster = null
                    set p = null
                    
                    set PSM[i] = PSM[PM]
                    set PSM[PM] = -2
                    set PM = PM - 1
                    
                    call destroy()
                    
                    if PM == -1 then
                        call PauseTimer(TIMER_2)
                    endif
                    
                endif

                set i = i + 1
                    
            endloop
                
        endmethod
        
        static method add takes unit l,unit c,real a returns nothing
            local thistype this = allocate()
            local integer lvl = GetUnitAbilityLevel(c,SPELL_ID)
            
            set PM = PM + 1
            set PSM[PM] = this
            
            set angle = a
            set light = l
            set caster = c
            set dmg = getDamage(caster,lvl)
            set heal = Heal(caster,lvl)
            set p = GetOwningPlayer(caster)
            
            if PM == 0 then
                call TimerStart(TIMER_2,PERIODIC,true,function thistype.onPeriodic)
            endif
        endmethod
        
    endstruct
endlibrary
ScreenShot
untit263.jpg

untit261.jpg

untit262.jpg
Changelog
v1.0: First released version.
v1.1: Minor bugs fixed.
v1.2: Minor bugs fixed.
v1.3: Minor bugs fixed.
Keywords:
magic,light
Contents

Just another Warcraft III map (Map)

Reviews
18th Apr 2016 Your resource has been reviewed by BPower. In case of any questions or for reconfirming the moderator's rating, please make use of the Quick Reply function of this thread. Review: Overall cool concept and presentation. The...

Moderator

M

Moderator

18th Apr 2016

General Info

Your resource has been reviewed by BPower.
In case of any questions or for reconfirming the moderator's rating,
please make use of the Quick Reply function of this thread.
Review:

Overall cool concept and presentation.
The code is so-so and leaves room for improvement. Read more in troubleshooting.
You could have considered that the caster can die while the spell is running.
The configuration options are a bit lousy for a level based spell.

You overdo white spaces without an obvious pattern. It hurts a bit the read-ability of your code.

Troubleshooting:

  • You don't run any debug checks, hence set PSM[PM] = -2 is totally useless.
    ---
  • You have tons of unnecessary GetUnitX/GetUnitY calls when creating the magic lights.
    ---
  • Stick to one function naming convention. For example the JPAG.
    In your code I found function Heal() right after function getDamage(). Terrible.
    ---
  • public struct MagicLightPlugin could preferably be a private struct.
    ---
  • You could merge the creation of magic lights into one seperate function to shorten your code.
    ---
  • The way you sorted your code compiles in a terrible amount of pseudo-code.
    Change the position of the two structs.
    ---
  • You are using magic numbers in your code which could preferably be replaced with named constants.
    For example the value .0174533 or 99999.
    ---
  • The dummy units could be paused to save extra computation time.
    ---
  • JASS:
    private constant function filterFunc takes unit filterUnit returns boolean
        return not IsUnitType(filterUnit,UNIT_TYPE_DEAD) and GetOwningPlayer(filterUnit) != SKIP_PLAYER and GetUnitTypeId(filterUnit) != 0
    endfunction
    ^It beats me that the JassHelper compiles that function. It shouldn't be constant.

Review changelog:
  1. -
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
You should allow the user to use his own damage/heal formula. Instead of this:
JASS:
private constant real DAMAGE_BASE = 20.
private constant real HEAL_BASE = 10.
use something like that:
JASS:
private function damage takes unit caster, unit target returns real
        return 20.
endfunction
private function heal takes unit caster, unit target returns real
        return 20.
endfunction

Didnt read the rest of the code.
 

TriggerHappy

Spell Moderator
Level 38
Joined
Jun 23, 2007
Messages
4,028
Couldnt find them here: http://www.wc3c.net/vexorian/jasshelpermanual.html

Anyway the function is not supposed to be constant, because thats the exact point of using functions instead of constant variables - they can return something which depends on the passed arguments. Something like "return 10.+GetHeroInt(caster, true)*0.1".

That's because it's a JASS feature.

JASS:
constant function foo takes nothing returns real
    return 5
endfunction
 
Level 19
Joined
Mar 18, 2012
Messages
1,717
You could store GetUnitX/Y before the loop.
Also you could make the unit filter part of the user configuration, not only because many tend to use UnitAlive over other is unit dead checks.

I think this one is actually constant for all casts -> da = 360./LIGHT_COUNT*.0174533

Concept and effects are great. Based on a quick scan, the code is neat and readable.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
1. Jasshelper inlines SOME stuff. Its rules for inlining are:

How to make a function inlineable? The current algorithm basically follows these rules (which are subject to change to allow more functions to be considered inlineable in the future):

The function is a one-liner
If the function is called by call the function's contents must begin with set or call or be a return of a single function.
If the inlined function is an assigment (set) it should not assign one of its arguments.
Every argument must be evaluated once and only once by the function, in the same order as they appear in the arguments list.
If the function contains function calls, they should all be evaluated after the arguments UNLESS the function is marked as non-state changing, at the moment some very few native functions and also return bug exploiters are considered as non-state changing.
(source)

As far as i can see it is completely irrelevant to the inliner whether a function is constant or not.

2. Other optimizers: There are other optimizers which can be run after or instead of JassHelper, for instance the Froptimizer from WurstPack. Froptimizer does a lot more optimizing than JassHelper (source) and its inliner is much more intelligent. But also for froptimizer it doesnt matter if a function is constant or not.

Maybe you confused constant functions with constant variables. Constant variables can of course be replaced (inlined) by their value.
 

TriggerHappy

Spell Moderator
Level 38
Joined
Jun 23, 2007
Messages
4,028
Hey, nice spell I can see your getting better at JASS, but as always here's some comments.

  • Inside onPeriodic, you could store GetUnitX/Y because it's called twice.
  • This has no requirements, therefor you should be usingscopeinstead oflibrarywhich is pretty standard for spells anyway.
  • I can't see where you destroy any MagicLight instance, so after 8191 casts this spell would break. Unless of course, I missed it? MagicLightPlugin is the only one that gets destroyed. Also, make sure to null all struct members that are handles (that get destroyed).
  • Consider using GroupUtils'sENUM_GROUPinstead of creating a new group each spell. You can make this optional by using static if's.
As far as i can see it is completely irrelevant to the inliner whether a function is constant or not.

I'm pretty sure you're right.

Constant functions are useful for configuration in vanilla JASS, and they may provide a small performance benefit.
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
Just benchmarked it with Sharpcraft :
JASS:
scope Stopwatch initializer onInit
    
    globals
        private constant integer ITERATIONS = 4000
    endglobals
    
    private function GetValue takes nothing returns integer
        return 5
    endfunction
    
    private constant function GetValu2 takes nothing returns integer
        return 5
    endfunction
    
    private function Actions takes nothing returns boolean
        local integer sw
        local integer i
        local real array ticks
        local string output
        local integer t

        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            set t = GetValue()
            set i = i + 1
        endloop
            
        set ticks[0] = StopWatchTicks(sw)
        set output = I2S(ITERATIONS) + " iterations of Test #1 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n"
        call StopWatchDestroy(sw)
            
        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            set t = GetValu2()
            set i = i + 1
        endloop
            
        set ticks[1] = StopWatchTicks(sw)
        set output = output + I2S(ITERATIONS) + " iterations of Test #2 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n\n"
        call StopWatchDestroy(sw)
            
        if (ticks[0] > ticks[1]) then
            set ticks[2] = 100 - (ticks[1]/ticks[0] * 100)
            set output = output + "Test #2 was " + R2S(ticks[2]) + "% faster than Test #1\n\n"
        else
            set ticks[2] = 100 - (ticks[0]/ticks[1] * 100)
            set output = output + "Test #1 is " + R2S(ticks[2]) + "% faster than Test #2\n\n"
        endif
        
        call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, output)
        
        return false
    endfunction

    //===========================================================================
    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddCondition(t, Condition(function Actions))
        set t=null
    endfunction

endscope

Result :
Test #2 was 9.365% faster than Test #1

On only 4000 iterations.
Now you have your proof.
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
Omg guys xD !!
When you can use constant functions without getting annoyed just use them it is free performance :O !!!

Testing with arguments....


EDIT : See the picture.
This is not a static percentage but the lowest I got is around 3% faster and the biggest was about 79% :O
The middle value is around 20-30%...

Test code :
JASS:
scope Stopwatch initializer onInit
    
    globals
        private constant integer ITERATIONS = 4000
    endglobals
    
    private function GetValue takes integer l returns integer
        return l*2
    endfunction
    
    private constant function GetValu2 takes integer l returns integer
        return l*2
    endfunction
    
    private function Actions takes nothing returns boolean
        local integer sw
        local integer i
        local real array ticks
        local string output
        local integer t

        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            set t = GetValue(i)
            set i = i + 1
        endloop
            
        set ticks[0] = StopWatchTicks(sw)
        set output = I2S(ITERATIONS) + " iterations of Test #1 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n"
        call StopWatchDestroy(sw)
            
        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            set t = GetValu2(i)
            set i = i + 1
        endloop
            
        set ticks[1] = StopWatchTicks(sw)
        set output = output + I2S(ITERATIONS) + " iterations of Test #2 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n\n"
        call StopWatchDestroy(sw)
            
        if (ticks[0] > ticks[1]) then
            set ticks[2] = 100 - (ticks[1]/ticks[0] * 100)
            set output = output + "Test #2 was " + R2S(ticks[2]) + "% faster than Test #1\n\n"
        else
            set ticks[2] = 100 - (ticks[0]/ticks[1] * 100)
            set output = output + "Test #1 is " + R2S(ticks[2]) + "% faster than Test #2\n\n"
        endif
        
        call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, output)
        
        return false
    endfunction

    //===========================================================================
    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddCondition(t, Condition(function Actions))
        set t=null
    endfunction

endscope
 

Attachments

  • Benchmark.png
    Benchmark.png
    99.8 KB · Views: 29
Level 14
Joined
Jun 27, 2008
Messages
1,325
I think you misunderstood what the difference between a constant and a non-constant funtion is. A constant function cannot make calls to non-constant functions, so inside the body of a constant function you can only use other constant functions.

So if you simply want to return 20 damage then a constant integer variable is the fastest solution:
JASS:
// Slow:
function damage takes ... returns integer
    return 20
endfunction

// Faster:
constant function damage takes ... returns integer
    return 20
endfunction

// Fastest:
constant integer DAMAGE = 20

Now lets say you want to allow the user to use an arbitrary damage formula, then you obviously cannot use a constant variable. So you can use:
JASS:
// Slow:
function damage takes unit caster, unit target returns integer
    return // ...
endfunction

// Faster:
constant function damage takes unit caster, unit target returns integer
    return // ...
endfunction

Now the problem you apparently dont understand is that in constant functions you can only call other constant functions. So lets say the spell should deal 3 times the casters intelligence, then this is what our code looks like:
JASS:
// Slow:
function damage takes unit caster, unit target returns integer
    return GetHeroInt(caster, true) * 3
endfunction

// Compile Error:
constant function damage takes unit caster, unit target returns integer
    return GetHeroInt(caster, true) * 3
endfunction

Now while the non-constant function works just fine the constant function will throw a compiler error:
"Call to non-constant function GetHeroInt in constant function"

This is because "GetHeroInt()" is a non-constant function: native GetHeroInt takes unit whichHero, boolean includeBonuses returns integer (see common.j)
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Yes but only if the user wants to use non constant func then you can remove it but the performance boost isn't negligable....
Id say if the user wants additional speed he should add the keyword, but adding it by default will cause "random" compile errors. Not such a cool thing..

Anyway, if a function can be made constant a good optimizer will do that for you.

Otherwise why in nearly each submission every time we say do make them constant zzz ??
The "because we always did" argument isnt very good. There are lots of stupid things we do daily..
 
Level 19
Joined
Mar 18, 2012
Messages
1,717
In onCast you have local real o for no reason.
Basically everything used more than twice should be stored into a local varriable. Variable lookups are just so much faster than function calls. I'm talking about GetUnitX/Y.

Store Player(15) into a player variable.

For super duper speed you could do : private static constant real inBetween = 360./LIGHT_COUNT*.0174533.

I know you use it in all submissions, but now it's time to tell you that the word "intervar" does not exist in the english language. :grin:
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Its really not important, but i think if a constant is only used by a certain structs methods it should be a member of the struct too.
I never heard that this is bad practice and i cant imagine why. Feel free to enlighten me if you have a reason.
 
Level 9
Joined
Dec 3, 2010
Messages
162
Using constant inside the struct is not that good pratice ...
In terms of definition of what's a struct/class ...

I personally think it would make more sense to encapsulate the constant within the struct if the specific constant were to be only used inside the struct and non-configurable. A good example would be let's say a constant called PERIOD. If you declare 2 structs within a library that requires a PERIOD constant with different values, you'd need to declare PERIOD1 and PERIOD2 separately in the global block.

And it's perfectly normal for classes/structs to have constants. For example, in Java, the Math class contains PI as a constant double. Not really sure why you would consider it a bad practice.
 
Level 1
Joined
May 29, 2014
Messages
1
I am new to this and I have a question ...

Because I install it as it says here, and then when I try to save the map I get hundreds of errors and tells me the detonator example "magic light" to been disabled ... Then I want to try and as expected no casts the spell .. . makes movement nothing more but the spell and / or effect never does, because many lines get error what is wrong? or what I'm doing wrong?

From already thank you friend
 

Attachments

  • PRUEBA.png
    PRUEBA.png
    106.1 KB · Views: 107
Last edited:
Top