• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[AI] Hero Creeping JASS AI

Level 14
Joined
Jan 10, 2023
Messages
248
Hello,

I am trying to make a very simple AI, but I am having trouble getting anything whatsoever going.

What I want is simply AI players to order their heroes to creep, the rest I intend to handle via triggers.

As you will see by my "script", I don't know anything about warcraft's AI, and I have been trying to understand it for days now and am not getting anywhere because I can't get a single thing to move. I know the code I'm sharing is just about worthless, but it might convey what I am trying to do here.

I don't want the AI player to care what the hero type is, I just want them order their captain to attack creep camps, ideally with the help of allies from time to time (or all the time).
Players have only a hero unit and whatever units they may summon, so the hero should be the only thing I have to worry about.

JASS:
//ATTACK
//------------------------------------------------

function AttackLoop takes nothing returns nothing
    local unit target = GetAllianceTarget()
    if target == null then
        set target = GetCreepCamp( 0, GetHeroLevelAI() * 9, false )
        call SetAllianceTarget( target )
        call AttackMoveKill( target )
        call ReformUntilTargetDead( target )
        call SleepInCombat()
        call InitAssault()
    endif
endfunction

//COMMAND Loop
//------------------------------------------------

function CommandLoop takes nothing returns nothing
    loop
        call AttackLoop()
    endloop
endfunction

//MAIN FUNCTION
//------------------------------------------------

function main takes nothing returns nothing
    call CampaignAI( 0, null )
    call DoCampaignFarms( false )
    call SetTargetHeroes( true )
    call SetRandomPaths( true )
    call SetDefendPlayer( true )
    call SetHeroesFlee( true )
    call SetHeroesBuyItems( true )
    call SetWatchMegaTargets( true )
    call SetHeroesTakeItems( true )
    call SetUnitsFlee( true )
    call SetGroupsFlee( true )
    call SetCaptainChanges( true )
    call SetCampaignAI()
    call CreateCaptains()
    call StartThread( function CommandLoop )
endfunction
 
Level 14
Joined
Jan 10, 2023
Messages
248
Bump.

I know this is no one's favorite topic to help a novice on, and I don't have much to work from, but I have put some more work in (to no avail) and will share if anyone can help, but assuming I don't get any help I'm just going to trigger it instead and let the AI just handle item pickup if that.
 
Level 30
Joined
Aug 29, 2012
Messages
1,383
At least there's no syntax error in the script so the issue comes from somewhere else

Have you tried using attack waves? It's not ideal but it's probably easier to set up, something like this

JASS:
    call InitAssaultGroup()
    call CampaignAttackerEx( 3,3,3, FOOTMAN )
    call CampaignAttackerEx( 2,2,2, RIFLEMAN )
    call SuicideOnPlayer(M1,Player(PLAYER_NEUTRAL_AGGRESSIVE))

Haven't tested if that works though
 
Level 21
Joined
Mar 16, 2008
Messages
958
Looks like you are missing the part that assembles the attacking unit group, picks a target, and also learns hero skills. Though you could learn the skills via trigger easily.

Here's what my script does, which is mostly based off the GUI AI Editor version.

I'm leaving out the global variable declarations and conditions functions (gCond_...) but I think you get the general idea.
Also there are some redundancies with the hero triggers but it seems when I remove those redundancies, the script stops working. So I just left those in.

JASS:
//***************************************************************************
//*
//*  Heroes
//*
//***************************************************************************

//===========================================================================
// Stores hero ID and skills
//===========================================================================
function SetHero takes integer order, integer heroid returns nothing
    if (order == 1) then
        set hero_id = heroid
        if (heroid == SOVEREIGN) then
            set skills1[ 1] = THROW_GOLD        /// 1
            set skills1[ 2] = KING_AURA         /// 1
            set skills1[ 3] = KING_COURT        /// 1
            set skills1[ 4] = ROYAL_BANISH      /// 1
            set skills1[ 5] = KING_AURA         /// 2
            set skills1[ 6] = THROW_GOLD        /// 2
            set skills1[ 7] = KING_COURT        /// 2
            set skills1[ 8] = ROYAL_BANISH      /// 2
            set skills1[ 9] = KING_AURA         /// 3
            set skills1[10] = THROW_GOLD        /// 3
            set skills1[11] = KING_COURT        /// 3
            set skills1[12] = KING_AURA         /// 4
            set skills1[13] = KING_COURT        /// 4
            set skills1[14] = ROYAL_BANISH      /// 3
        elseif (heroid == MONARCH) then
            set skills1[ 1] = ROYAL_SHOCKWAVE   /// 1
            set skills1[ 2] = KING_AURA         /// 1
            set skills1[ 3] = KING_COURT        /// 1
            set skills1[ 4] = ROYAL_BANISH      /// 1
            set skills1[ 5] = KING_AURA         /// 2
            set skills1[ 6] = ROYAL_SHOCKWAVE   /// 2
            set skills1[ 7] = KING_COURT        /// 2
            set skills1[ 8] = ROYAL_BANISH      /// 2
            set skills1[ 9] = KING_AURA         /// 3
            set skills1[10] = ROYAL_SHOCKWAVE   /// 3
            set skills1[11] = KING_COURT        /// 3
            set skills1[12] = KING_AURA         /// 4
            set skills1[13] = KING_COURT        /// 4
            set skills1[14] = ROYAL_BANISH      /// 3
        elseif (heroid == MAJESTY) then
            set skills1[ 1] = ROYAL_BLINK       /// 1
            set skills1[ 2] = KING_AURA         /// 1
            set skills1[ 3] = RYL_FRK_LGHTNNG   /// 1
            set skills1[ 4] = ROYAL_BANISH      /// 1
            set skills1[ 5] = KING_AURA         /// 2
            set skills1[ 6] = ROYAL_BLINK       /// 2
            set skills1[ 7] = RYL_FRK_LGHTNNG   /// 2
            set skills1[ 8] = ROYAL_BANISH      /// 2
            set skills1[ 9] = KING_AURA         /// 3
            set skills1[10] = ROYAL_BLINK       /// 3
            set skills1[11] = RYL_FRK_LGHTNNG   /// 3
            set skills1[12] = KING_AURA         /// 4
            set skills1[13] = RYL_FRK_LGHTNNG   /// 4
            set skills1[14] = ROYAL_BANISH      /// 3
        elseif (heroid == MAGE) then
            set skills1[ 1] = HOLY_BOLT
            set skills1[ 2] = DEVOTION_AURA
            set skills1[ 3] = ROYAL_BLINK_PRINCESS
            set skills1[ 4] = ROYAL_BLIZ_PRINCESS
            set skills1[ 5] = HOLY_BOLT
            set skills1[ 6] = ROYAL_VOODOO
            set skills1[ 7] = DEVOTION_AURA
            set skills1[ 8] = ROYAL_BLINK_PRINCESS
            set skills1[ 9] = ROYAL_BLIZ_PRINCESS
            set skills1[10] = HOLY_BOLT
            set skills1[11] = DEVOTION_AURA
            set skills1[12] = ROYAL_BLIZ_PRINCESS
            set skills1[13] = ROYAL_BLINK_PRINCESS
            set skills1[14] = ROYAL_VOODOO
        elseif (heroid == PALADIN) then
            set skills1[ 1] = HOLY_BOLT
            set skills1[ 2] = DEVOTION_AURA
            set skills1[ 3] = ROYAL_DIVINE_SHIELD
            set skills1[ 4] = ROYAL_SEAL_OF_RIGHT
            set skills1[ 5] = HOLY_BOLT
            set skills1[ 6] = ROYAL_REZ
            set skills1[ 7] = DEVOTION_AURA
            set skills1[ 8] = ROYAL_DIVINE_SHIELD
            set skills1[ 9] = ROYAL_SEAL_OF_RIGHT
            set skills1[10] = HOLY_BOLT
            set skills1[11] = DEVOTION_AURA
            set skills1[12] = ROYAL_SEAL_OF_RIGHT
            set skills1[13] = ROYAL_DIVINE_SHIELD
            set skills1[14] = ROYAL_REZ
        elseif (heroid == WARRIOR) then
            set skills1[ 1] = ROYAL_BLADESTORM
            set skills1[ 2] = DEVOTION_AURA
            set skills1[ 3] = ROYAL_STORM_BOLT
            set skills1[ 4] = HOLY_BOLT
            set skills1[ 5] = ROYAL_BLADESTORM
            set skills1[ 6] = ROYAL_AVATAR
            set skills1[ 7] = DEVOTION_AURA
            set skills1[ 8] = ROYAL_STORM_BOLT
            set skills1[ 9] = HOLY_BOLT
            set skills1[10] = ROYAL_BLADESTORM
            set skills1[11] = DEVOTION_AURA
            set skills1[12] = HOLY_BOLT
            set skills1[13] = ROYAL_STORM_BOLT       
            set skills1[14] = ROYAL_AVATAR
        endif
    elseif (order == 2) then
        set hero_id2 = heroid
        if (heroid == SOVEREIGN) then
            set skills2[ 1] = KING_AURA         /// 1
            set skills2[ 2] = THROW_GOLD        /// 1
            set skills2[ 3] = KING_COURT        /// 1
            set skills2[ 4] = ROYAL_BANISH      /// 1
            set skills2[ 5] = KING_AURA         /// 2
            set skills2[ 6] = THROW_GOLD        /// 2
            set skills2[ 7] = KING_COURT        /// 2
            set skills2[ 8] = ROYAL_BANISH      /// 2
            set skills2[ 9] = KING_AURA         /// 3
            set skills2[10] = THROW_GOLD        /// 3
            set skills2[11] = KING_COURT        /// 3
            set skills2[12] = KING_AURA         /// 4
            set skills2[13] = KING_COURT        /// 4
            set skills2[14] = ROYAL_BANISH      /// 3
        elseif (heroid == MONARCH) then
            set skills2[ 1] = KING_AURA         /// 1
            set skills2[ 2] = ROYAL_SHOCKWAVE   /// 1
            set skills2[ 3] = KING_COURT        /// 1
            set skills2[ 4] = ROYAL_BANISH      /// 1
            set skills2[ 5] = KING_AURA         /// 2
            set skills2[ 6] = ROYAL_SHOCKWAVE   /// 2
            set skills2[ 7] = KING_COURT        /// 2
            set skills2[ 8] = ROYAL_BANISH      /// 2
            set skills2[ 9] = KING_AURA         /// 3
            set skills2[10] = ROYAL_SHOCKWAVE   /// 3
            set skills2[11] = KING_COURT        /// 3
            set skills2[12] = KING_AURA         /// 4
            set skills2[13] = KING_COURT        /// 4
            set skills2[14] = ROYAL_BANISH      /// 3
        elseif (heroid == MAJESTY) then
            set skills2[ 1] = KING_AURA         /// 1
            set skills2[ 2] = ROYAL_BLINK       /// 1
            set skills2[ 3] = RYL_FRK_LGHTNNG   /// 1
            set skills2[ 4] = ROYAL_BANISH      /// 1
            set skills2[ 5] = KING_AURA         /// 2
            set skills2[ 6] = ROYAL_BLINK       /// 2
            set skills2[ 7] = RYL_FRK_LGHTNNG   /// 2
            set skills2[ 8] = ROYAL_BANISH      /// 2
            set skills2[ 9] = KING_AURA         /// 3
            set skills2[10] = ROYAL_BLINK       /// 3
            set skills2[11] = RYL_FRK_LGHTNNG   /// 3
            set skills2[12] = KING_AURA         /// 4
            set skills2[13] = RYL_FRK_LGHTNNG   /// 4
            set skills2[14] = ROYAL_BANISH      /// 3
        elseif (heroid == MAGE) then
            set skills2[ 1] = DEVOTION_AURA
            set skills2[ 2] = HOLY_BOLT
            set skills2[ 3] = ROYAL_BLINK_PRINCESS
            set skills2[ 4] = ROYAL_BLIZ_PRINCESS
            set skills2[ 5] = DEVOTION_AURA
            set skills2[ 6] = ROYAL_VOODOO
            set skills2[ 7] = HOLY_BOLT
            set skills2[ 8] = ROYAL_BLINK_PRINCESS
            set skills2[ 9] = ROYAL_BLIZ_PRINCESS
            set skills2[10] = DEVOTION_AURA
            set skills2[11] = HOLY_BOLT
            set skills2[12] = ROYAL_BLIZ_PRINCESS
            set skills2[13] = ROYAL_BLINK_PRINCESS
            set skills2[14] = ROYAL_VOODOO
        elseif (heroid == PALADIN) then
            set skills2[ 1] = DEVOTION_AURA
            set skills2[ 2] = HOLY_BOLT
            set skills2[ 3] = ROYAL_DIVINE_SHIELD
            set skills2[ 4] = ROYAL_SEAL_OF_RIGHT
            set skills2[ 5] = DEVOTION_AURA
            set skills2[ 6] = ROYAL_REZ
            set skills2[ 7] = HOLY_BOLT
            set skills2[ 8] = ROYAL_DIVINE_SHIELD
            set skills2[ 9] = ROYAL_SEAL_OF_RIGHT
            set skills2[10] = DEVOTION_AURA
            set skills2[11] = HOLY_BOLT
            set skills2[12] = ROYAL_SEAL_OF_RIGHT
            set skills2[13] = ROYAL_DIVINE_SHIELD
            set skills2[14] = ROYAL_REZ
        elseif (heroid == WARRIOR) then
            set skills2[ 1] = DEVOTION_AURA
            set skills2[ 2] = ROYAL_BLADESTORM
            set skills2[ 3] = ROYAL_STORM_BOLT
            set skills2[ 4] = HOLY_BOLT
            set skills2[ 5] = DEVOTION_AURA
            set skills2[ 6] = ROYAL_AVATAR
            set skills2[ 7] = ROYAL_BLADESTORM
            set skills2[ 8] = ROYAL_STORM_BOLT
            set skills2[ 9] = HOLY_BOLT
            set skills2[10] = DEVOTION_AURA
            set skills2[11] = ROYAL_BLADESTORM
            set skills2[12] = HOLY_BOLT
            set skills2[13] = ROYAL_STORM_BOLT       
            set skills2[14] = ROYAL_AVATAR
        endif
    endif
endfunction

//===========================================================================
// Selects hero IDs for three possible heroes
//===========================================================================
function SelectHeroes takes nothing returns nothing
    local integer roll = GetRandomInt(1,9)
    if (roll == 1) then
        call SetHero( 1, MAGE )
        call SetHero( 2, SOVEREIGN )
    elseif (roll == 2) then
        call SetHero( 1, PALADIN )
        call SetHero( 2, SOVEREIGN )
    elseif (roll == 3) then
        call SetHero( 1, WARRIOR )
        call SetHero( 2, SOVEREIGN )
    elseif (roll == 4) then
        call SetHero( 1, MAGE )
        call SetHero( 2, MONARCH )
    elseif (roll == 5) then
        call SetHero( 1, PALADIN )
        call SetHero( 2, MONARCH )
    elseif (roll == 6) then
        call SetHero( 1, WARRIOR )
        call SetHero( 2, MONARCH )
    elseif (roll == 7) then
        call SetHero( 1, MAGE )
        call SetHero( 2, MAJESTY )
    elseif (roll == 8) then
        call SetHero( 1, PALADIN )
        call SetHero( 2, MAJESTY )
    elseif (roll == 9) then
        call SetHero( 1, WARRIOR )
        call SetHero( 2, MAJESTY )
    endif
endfunction

//===========================================================================
// Returns the hero skill for the given hero and level
//===========================================================================
function ChooseHeroSkill takes nothing returns integer
    local integer curHero = GetHeroId()
    local integer level = GetHeroLevelAI()
    if (level > max_hero_level) then
        set max_hero_level = level
    endif
  
    if (curHero == hero_id) then
        return skills1[level]
    elseif (curHero == hero_id2) then
        return skills2[level]
    endif
    return 0
endfunction
JASS:
function BuildPriorities takes nothing returns nothing
    local integer mine = TownWithMine()
    call SetBuildAll( BUILD_UNIT, 1, hero_id2, -1 )
    if (gCond_Edict_GT_2 and gCond_Food_Space_GT4) then
        call SetBuildAll( BUILD_UNIT, 1, hero_id, -1 )
    endif 
...
JASS:
//***************************************************************************
//*
//*  Attacking
//*
//***************************************************************************

//===========================================================================
// Returns true if the minimum forces for an attack exist
//===========================================================================
function HaveMinimumAttackers takes nothing returns boolean
    local integer count

    //  Check for attack wave limit
    if (attackWave > 21) then
        return false
    endif

    // Atleast 12 units
    if (gCond_Low_Army) then
        return false
    endif

    return true
endfunction

//===========================================================================
// Assigns units to attack based on the given attack group
//===========================================================================
function PrepareAttackGroup takes integer groupID returns nothing
    local integer all

    // Attack Group #1: All Units
    if (groupID == 1) then
    set all = GetUnitCountDone( hero_id )
    call SetAssaultGroup( all, all, hero_id )
    if (gCond_Gyrations) then
        set all = GetUnitCountDone( hero_id2 )
        call SetAssaultGroup( all, all, hero_id2 )
    endif
    set all = GetUnitCountDone( MORTAR )
    call SetAssaultGroup( all, all, MORTAR )
    set all = GetUnitCountDone( ROYAL_PRIEST )
    call SetAssaultGroup( all, all, ROYAL_PRIEST )
    set all = GetUnitCountDone( BISHOP )
    call SetAssaultGroup( all, all, BISHOP )
    set all = GetUnitCountDone( KNIGHT )
    call SetAssaultGroup( all, all, KNIGHT )
    set all = GetUnitCountDone( RED_MARSHALL )
    call SetAssaultGroup( all, all, RED_MARSHALL )
    set all = GetUnitCountDone( BLUE_MARSHALL )
    call SetAssaultGroup( all, all, BLUE_MARSHALL )
    set all = GetUnitCountDone( TEAL_MARSHALL )
    call SetAssaultGroup( all, all, TEAL_MARSHALL )
    set all = GetUnitCountDone( PURP_MARSHALL )
    call SetAssaultGroup( all, all, PURP_MARSHALL )
    set all = GetUnitCountDone( SQUIRE )
    call SetAssaultGroup( all, all, SQUIRE )
    set all = GetUnitCountDone( HIGH_FOOTMAN )
    call SetAssaultGroup( all, all, HIGH_FOOTMAN )
    set all = GetUnitCountDone( EXEGGCUTER )
    call SetAssaultGroup( all, all, EXEGGCUTER )
    set all = GetUnitCountDone( SPEAR_MAN )
    call SetAssaultGroup( all, all, SPEAR_MAN )
    set all = GetUnitCountDone( SORCERESS )
    call SetAssaultGroup( all, all, SORCERESS )
    set all = GetUnitCountDone( KNIGHT )
    call SetAssaultGroup( all, all, KNIGHT )
    set all = GetUnitCountDone( ROYAL_KNIGHT_2 )
    call SetAssaultGroup( all, all, ROYAL_KNIGHT_2 )
    set all = GetUnitCountDone( ROYAL_KNIGHT_3 )
    call SetAssaultGroup( all, all, ROYAL_KNIGHT_3 )
    set all = GetUnitCountDone( TEMPLAR )
    call SetAssaultGroup( all, all, TEMPLAR )
    set all = GetUnitCountDone( JUSTICAR )
    call SetAssaultGroup( all, all, JUSTICAR )
    set all = GetUnitCountDone( ROYAL_KNIGHT_4 )
    call SetAssaultGroup( all, all, ROYAL_KNIGHT_4 )
    set all = GetUnitCountDone( GENERAL )
    call SetAssaultGroup( all, all, GENERAL )
    set all = GetUnitCountDone( CRUSADER_ELITE )
    call SetAssaultGroup( all, all, CRUSADER_ELITE )
    set all = GetUnitCountDone( ROYAL_SPEAR_ELITE )
    call SetAssaultGroup( all, all, ROYAL_SPEAR_ELITE )
    set all = GetUnitCountDone( GENERAL_ELITE )
    call SetAssaultGroup( all, all, GENERAL_ELITE )
    set all = GetUnitCountDone( PEGASUS )
    call SetAssaultGroup( all, all, PEGASUS )
    set all = GetUnitCountDone( EXEMPLAR_XL )
    call SetAssaultGroup( all, all, EXEMPLAR_XL )
    set all = GetUnitCountDone( ROYAL_COURT_MAGE )
    call SetAssaultGroup( all, all, ROYAL_COURT_MAGE )
    endif
endfunction

//===========================================================================
// Prepares an attack group based on the current attack wave
//===========================================================================
function PrepareForces takes nothing returns nothing
        call PrepareAttackGroup( 1 )
endfunction

//===========================================================================
// Sleep delays for each attack wave
//===========================================================================
function AttackWaveDelay takes integer inWave returns nothing
    if (inWave < nextDelay) then
        return
    endif
    if (inWave == 0) then
        call Sleep( 120 )
    elseif (inWave == 1) then
        call Sleep( 100 )
    elseif ((inWave == 2) and (Tier == 1)) then  
        call Sleep( 100 )
    elseif ((inWave == 2) and (Tier == 2)) then 
        call Sleep( 100 )
    elseif ((inWave == 2) and (Tier == 3)) then 
        call Sleep( 100 )
    endif
    set nextDelay = inWave + 1
endfunction

//===========================================================================
// Advances attack wave counter
//===========================================================================
function AttackWaveUpdate takes nothing returns nothing
    call AttackWaveDelay( attackWave )
    set attackWave = attackWave + 1
    if (attackWave > 2) then
        set attackWave = 2
        set nextDelay = attackWave + 1
    endif
endfunction

//===========================================================================
// Basic attack functionality
//===========================================================================
function AttackTarget takes unit target, boolean addAlliance returns nothing
    if (target == null) then
        return
    endif
    if (addAlliance) then
        call SetAllianceTarget( target )
    endif
    call FormGroup( 3, true )
    call AttackMoveKillA( target )
    if (not addAlliance) then
        call SetAllianceTarget( null )
    endif
endfunction

//===========================================================================
// Initiates an attack based on target priorities
//===========================================================================
function LaunchAttack takes nothing returns nothing
    local unit target = null
    local boolean setAlly = true
    // Don't launch any attack while town is threatened
    if (TownThreatened()) then
        call Sleep( 2 )
        return
    endif

    //Target Priority #1 ---- disabled for rush
    if (Tier < 3) then
        if (target == null) then
            set target = GetCreepCamp( 0, 9, false )
        endif
    endif

    // Target Priority #2
    if (target == null) then
        set target = GetEnemyExpansion()
        if (target == null) then
            call StartGetEnemyBase(  )
            loop
                exitwhen (not WaitGetEnemyBase())
                call SuicideSleep( 1 )
            endloop
            set target = GetEnemyBase()
        endif
    endif

    // Attack the target and increment attack wave
    if (target != null) then
        call AttackTarget( target, setAlly )
        call AttackWaveUpdate(  )
    else
        // If no target was found, sleep a bit before trying again
        call Sleep( 20 )
    endif
endfunction

//===========================================================================
// Determines all attacking assignments
//===========================================================================
function AttackAssignment takes nothing returns nothing
    call StaggerSleep( 0, 2 )
    if (attackWave == 1) then
        call AttackWaveDelay( 0 )
    endif
    loop
        loop
            call UpdateConditions(  )
            exitwhen (HaveMinimumAttackers() and not CaptainRetreating())
            call Sleep( 2 )
        endloop
        call RemoveInjuries(  )
        call ResetAttackUnits(  )
        call PrepareForces(  )
        call LaunchAttack(  )
    endloop
endfunction
JASS:
//***************************************************************************
//*
//*  Main Entry Point
//*
//***************************************************************************

//===========================================================================
function main takes nothing returns nothing
    call InitAI(  )
    call SetPlayerName( ai_player, "Techeron Rex II" )
    call InitOptions(  )
    call SelectHeroes(  )
    call CreateCaptains(  )
    call SetHeroLevels( function ChooseHeroSkill )

    call Sleep( 0.1 )
    call StartThread( function WorkerAssignment )
    call StartThread( function AttackAssignment )
    call StartThread( function ConditionLoop )
    call PlayGame(  )
endfunction

There are also other attack functions defined in common.ai, such a suicide, but I don't fully understand nor use those functions. I'd reccomend looking that over as well as just generate a basic script from the GUI AI Editor as a base template. There are also good guides from Nowow and InsaneMonster on Hive but they focus more on Campaign AIs but some can still be applicable to either type.
 

Attachments

  • common.ai
    96.1 KB · Views: 1
  • King_AI_normal_new_02.ai
    69.7 KB · Views: 1
Last edited:
Top