mircscripting.info Forum Index mircscripting.info
#mIRCscripting Forum
 
 FAQFAQ   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   RegisterRegister 
 ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in 

Intro to mIRC scripting

 
Post new topic   Reply to topic    mircscripting.info Forum Index -> Tutorials
View previous topic :: View next topic  
Author Message
ralarX
Quite Active


Joined: 23 Apr 2005
Posts: 45

PostPosted: Sun Jan 20, 2008 2:16 pm    Post subject: Intro to mIRC scripting Reply with quote

There was still missing a ground-up tutorial, this is my attempt.

=== AN INTRO TO SCRIPTING IN THE IRC CLIENT mIRC. ===

It's done in menu Tools/Scripts Editor, it has tabs Aliases | Popups | Remote | Users | Variables.

+++ ALIASES +++
These are modular pieces of code, functions.
You call them by their name, pass them some parameters (pieces of data) if needed, and they return a value, if needed.
Usually this Aliases section contains very simple functions, Remote tab is used for more complicated ones.

Example:
/op /mode # +ooo $$1 $2 $3
This is called a "command line".
This code means that if you type "/op paul fred bill" in a mIRC channel window,
mIRC will search section Aliases for "/op" or tab Remote for "alias op",
passes it 'paul fred bill' as data, and will then execute the commands after it, or, required if more code lines, within a { commandshere } enclosure.

The alias (= function) receives your command-line data (here "paul fred bill") in special memory places named $<number>
the <number> is the Nth space-separated word, here $1 is "paul", $2 is "fred", $3 is "bill".
Combinations can be used, for ex. $1-2 is "paul fred" and $1- s "paul fred bill".

Aliases can also be passed data in another way than command-line parameters:
The "#" in the alias/function code is a symbol that represents the name of the 'current' channel, here the one where you typed the command "/op paul fred bill".

So, all together, the final result is that this alias will send a command to the IRC server:
"mode #channelnamehere +ooo paul fred bill"
This will make the 3 given nicks channel-operators (if all required conditions are met).

Noticed that $$1 ? It wasn't a typo, the double $$ makes mIRC stop the execution attempt right there.
So if you only typed /op, without nicks, nothing will be done.
It's a good practice to not let mIRC or the IRC server spit out avoidable errors.

+++ POPUPS +++
These are menu items that appear ("popup") when you rightclick in mIRC.
Click on View, you see location sections named Status, Channel, Query, Nick List, Menubar.
Every location section corresponds to a certain interface part of mIRC.
- Status: IRC connection window
- Channel: IRC channel window
- Query: IRC private message window
- Nick List: the area with the present nicks in the channel window.
- Menubar: under the Commands item in mIRCs main menubar.

Example in Nick List section:
Query:/query $$1
This shows a menu item named "Query" and when clicked, it will execute the command "/query $$1".
In this case, $1 holds the selected nick, so this will create a query window to start talking private to that nick.

+++ USERS +++
Here you can give your IRC friends access to your scripts.
On IRC, a user is recognized with something called "mask".
This allows to recognize a user with one single string, so the IRC server is able to pass this data in one message.
It has the format nick!user@host.
- nick: the users IRC name.
- user: the users reply to IDENTD protocol request (beyond scope of this tutor)
- host: the users address.
The ! and @ symbols separate these 3 parts.
Example:
John!yayhurah@D812D9E5.nsw.bigpond.net.au
A "mask" represents this user.
If you wanted to ban this person from your IRC channel, regardless the nick or user he has,
you type the command "/mode #yourchannel +b *!*@D812D9E5.nsw.bigpond.net.au".
This introduces symbols named "wildcards".
mIRC knows 3 of them:
* means any string of any amount characters including none.
? means any single character on that position.
& means any space-separated word on that position.
If you would type "/mode #yourchannel +b *!*@*" then it would block all users from joining your channel,
and make all currently present regulars (no op, halfop or voice) unable to talk.

Back to this Scripts Editor section: you can couple a "level" to user masks here.
A "level" is a way to give other people a certain access to your script.
People that do not have the required level will not cause execution of your script.
mIRC will then execute code that reacts ("triggers") on what all happens ("events") only for the users matching the given level.
Example:
100:John!*@*.nsw.bigpond.net.au
This will assign a level named "100" to above user-mask, the nick needs to be "John" and the address needs to end in ".nsw.bigpond.net.au".

If this user now talks on any IRC channel you are on, and you have in Scripts Editor tab "Remote" (see further), this code ("event") to execute:
Code:

on 100:TEXT:*:#: { msg # Hey I'm a parrot! $nick said $1- }

... then this script will repeat everything John says on any channel you (- and of course John) are on.

If you wanted to make this work for everybody, regardless Scripts Editor tab Users, replace the "100" level with "*".

+++ VARIABLES +++
This Scripts Editor tab just shows the currently existing "global" variables.
Variables are memory places that store a single line of text.
They are referenced by their name, which must have a "%" as first character.
Example, type:
/set %hellomessage Hello, I am $nick $+ , I'm 5 years old and I want... alot!
This will create (or overwrite, if it already exist) a variable named "%hellomessage", containing the given text.
mIRC stores the variables in a file, so do not get lost when mIRC is shut down.

+++ REMOTE +++
And finally, I put it last, the Scripts Editor tab Remote.
All that can be done in tabs Aliases, Popups and Users can also be done here.
mIRC sees the beginning of every new line as something to execute.
If you started a script line with "Hi" it would try to find an alias named "Hi" and when not found, spit out error "Unknown command".
You can put more commands on the same line by separating them with a | symbol (a space on both sides)
a line like "dothis | dothat | dolast" would try to execute 3 aliases with those names.

This same alias from Aliases:
/op /mode # +ooo $$1 $2 $3
can be coded here as:
alias op { mode # +ooo $$1 $2 $3 }
The "alias" prefix here is the only (and required) difference.
You have to make sure that no two aliases have identical names.

This is also the time to bring up "identifiers".
They are as easy as aliases, in fact, they are just the same code.
The only difference is the way you let the code run ("call it").

- The call as /command:
/aliasnamehere parameter1 parameter2 parameter3

- The call as $identifier:
/noop $aliasnamehere(parameter1,parameter2,parameter3)
(noop is a dummy command, it's needed to avoid mirc 'seeing' the value that the alias may return as code to execute.

How you call the alias, is just a practical choice.

The data (parameters) passed to the alias (= function) are:
- in case /command call: separated by spaces.
- in case $identifier call: separated by comma's.
If the function executes a "/return somevalue" command:
- in case /command call: the mIRC identifier $result contains "somevalue".
- in case $identifier call: it's directly available:
/noop $aliasnamehere(parameter1,parameter2,parameter3)
can be:
/var %result = $aliasnamehere(parameter1,parameter2,parameter3)
Then the variable named "%result" contains "somevalue".

And as extra in Scripts Editor tab Remote, pieces of code called "events" should be put here.
Events are again just some code, like functions.
The first difference is (again!) the calling method.
Events are called by mIRC itself, or by the mIRC-script /signal command.
The second difference is that they have some conditions "built-in".
Those determine if the event should be executed or not.
Code:

on 100:TEXT:*:#: { msg # Hey I'm a parrot! $nick said $1- }

Remember this example?
Here the conditions are:
- The user that speaks should have a level "100" (see Scripts Editor tab Users)
- It must be a TEXT IRC-occurrence
- It can be any text (wildcard *)
- It must be typed on a channel
- It can be any channel, since the # symbol there means that.

Suppose someone speaks on an IRC channel.
mIRC will look through all loaded scriptfiles for events with conditions-set so that they tell mIRC to execute (= trigger) it.
If mIRC finds one, it will execute it, and then proceed directly to the next loaded scriptfile.

This has an important consequence: you cannot have a second event with same conditions-set in the same scriptfile.
If you run different-purpose scripts, it is recommended to group them on purpose in their own scriptfile.
If it occurs a single-purpose but somewhat bigger script single-file needs two pieces code to run, you have to combine them into one single event.
Example:
Code:

on *:TEXT:*:#: {
msg # Hey I'm a parrot on all channels! $nick said $1-
}
on *:TEXT:*:#: {
if ($istok($1-,hello,32)) { msg # Hello to you too, $nick $+ ! }
}

These can be combined in:
Code:

on *:TEXT:*:#: {
if ($istok($1-,hello,32)) { msg # Hello to you too, $nick $+ ! }
else { msg # Hey I'm a parrot on all channels! $nick said $1- }
}

In this combination, if the person John has the word "hello" in his text, it will answer "Hello to you too, John!".
If not, it will show the parrot message.
It's not exactly the same working principle as above two separate events.
(It does not make much sense to say both lines)

=== BUILDING UP A (BIGGER) SCRIPT ===

+++ MODULARITY +++

A somewhat bigger "script" has both functions and events.
Events usually start the execution of code, directly in the event { } code enclosure or in called functions.
Functions allow to re-use pieces of code, which can make the script-code alot shorter.
Example:
Code:

on *:TEXT:hello:#: { msg $nick Hello to you too, $nick $+ ! }
on *:TEXT:hi:#: { msg $nick Hello to you too, $nick $+ ! }
on *:TEXT:hey:?: { msg $nick Hello to you too, $nick $+ ! }

can be:
Code:

on *:TEXT:hello:#: { greet }
on *:TEXT:hi:#: { greet }
on *:TEXT:hey:?: { greet }
alias -l greet { msg $nick Hello to you too, $nick $+ ! }

- Note1:
The "-l" after "alias" is a "command switch".
It means the function named "greet" exists only in this scriptfile.
So other scriptfiles can have the same alias name, it will not interfere / cause problems.
- Note2:
There is no need to use "greet $nick" instead of "greet", since the native mIRC-identifier "$nick" also exists in any called functions.
Most of mIRC's native identifiers behave like that, with the exception of the $1 $2 $3 $1-2 etc identifiers.

When your script grows in functionality, a modular and clear structure becomes more and more important.
If you do not take care of this, a bigger project quickly leads to either abandon it either rewriting most of it.
Therefore, some hints:
- Make code reusable.
If you write a function, do it so that it's as universal usable as possible, i.e., directly usable (or at least easy to convert it so) by other script.
While the script grows, make such general function base, you soon run into the chance to simply reuse existing functions instead of making new ones.
If this succeeds, the top of the executing code chain (so the event where the execution started) contains almost only calls to functions.
This also leads to versatile functions that call alot specific-purpose functions.
Example:
Suppose you want a function to bankick all users matching a certain nick!user@host mask, on all the channels you are op on, and on all your networks.
The bad way is to write one alias containing 3 nested (inside eachother) loops, level 0 for networks, level 1 for channels and level 2 for nicks on the channels.
You don't need to understand the actual code for this, though you can always check mIRC's /help
Code:

; Bankick on all networks <mask>
alias bankickglobal {
  var %i1 = 1
  while ($scon(%i1)) {
    if ($scon(%i1).status != connected) { inc %i1 | continue }
    scid $scon(%i1)
    var %i2 = 1
    while ($comchan($me,%i2)) {
      var %chan = $v1
      if ($me !isop %chan) { inc %i2 | continue }
      var %i3 = 1, %ban
      while ($nick(%chan,%i3)) {
        var %nick = $v1
        if ($1 iswm $address(%nick,5)) {
          if (!%ban) { var %ban = 1 | ban -u600 %chan $1 }
          kick %chan %nick byebye.
        }
        inc %i3
      }
      inc %i2
    }
    inc %i1
  }
}

; Bankick on given <network> <mask>
alias bankicknet {
  var %cid  = $netcid($1) | if (!%cid) { return }
  if (%cid != $cid) { scid %cid }
  var %i1 = 1
  while ($comchan($me,%i1)) {
    var %chan = $v1
    if ($me !isop %chan) { inc %i1 | continue }
    var %i2 = 1, %ban
    while ($nick(%chan,%i2)) {
      var %nick = $v1
      if ($2 iswm $address(%nick,5)) {
        if (!%ban) { var %ban = 1 | ban -u600 %chan $2 }
        kick %chan %nick byebye.
      }
      inc %i2
    }
    inc %i1
  }
}

;  Bankick on the given <network> <channel> <mask>
alias bankickchan {
  var %cid  = $netcid($1) | if (!%cid) { return }
  if (%cid != $cid) { scid %cid }
  if ($me !isop $2) { return }
  var %i = 1, %ban
  while ($nick($2,%i)) {
    var %nick = $v1
    if ($3 iswm $address(%nick,5)) {
      if (!%ban) { var %ban = 1 | ban -u600 $2 $3 }
      kick $2 %nick byebye.
    }
    inc %i
  }
}
; Convert network name to connection id
; Input: network
; Output: CID value or 0
alias -l netcid {
  var %total = $scon(0), %i = 1
  while (%i <= %total) { if ($scon(%i).network == $1) && ($scon(%i).status == connected) { return $scon(%i) } | inc %i }
  return 0
}

The good way is this:
Code:

; Bankick on all networks <mask>
alias bankickglobal { scid -at1 bankicknet $!network $1 }

; Bankick on given <network> <mask>
alias bankicknet {
  var %cid  = $netcid($1) | if (!%cid) { return }
  if (%cid != $cid) { scid %cid }
  var %i = 1 | while ($comchan($me,%i)) { bankickchan $1 $v1 $2 | inc %i }
}

;  Bankick on the given <network> <channel> <mask>
alias bankickchan {
  var %cid  = $netcid($1) | if (!%cid) { return }
  if (%cid != $cid) { scid %cid }
  if ($me !isop $2) { return }
  var %i = 1, %ban
  while ($nick($2,%i)) {
    var %nick = $v1
    if ($3 iswm $address(%nick,5)) {
      if (!%ban) { var %ban = 1 | ban -u600 $2 $3 }
      kick $2 %nick byebye.
    }
    inc %i
  }
}

; Convert network name to connection id
; Input: network
; Output: CID value or 0
alias -l netcid {
  var %total = $scon(0), %i = 1
  while (%i <= %total) { if ($scon(%i).network == $1) && ($scon(%i).status == connected) { return $scon(%i) } | inc %i }
  return 0
}

Notice how much shorter and more 'clean' the modular code is.

+++ ORGANISATION +++
A bigger script with alot different functions needs it, so it's best to start any script taking this into account.
You never know where you end up.
Practically, it comes down to grouping code together according to what it does.
This code group should not have any external direct reference.

Example:
A list of people you want to autovoice upon join, stored in a file.
A set of aliases is created:
alias VoiceList_Add { ... }
alias VoiceList_Remove { ... }
alias VoiceList_Check { ... }
alias VoiceList_Show { ... }

Notice how the aliases are named to make clear they belong to the VoiceList script feature.
Only these aliases are allowed to access the file.
Take for example that you want to show the VoiceList file content in a channel, when someone types !showvoices.
You write an event (on TEXT) in some scriptfile with several different !commands and it reads the VoiceList file, adds colors etc.
That is a NOT-done. Why? If you ever need to change something in the VoiceList layout, you have to edit on several places,
and it's easily to forget a place if the direct references to the VoiceList datafile are scattered around.
The way to do it is calling VoiceList_Show, which does the output formatting and for example returns the name of a temp file with the output.
The !showvoices event shows the content in the channel and then deletes the temp file.
And thus no direct access anymore.
If you ever want to add a new parameter to the VoiceList entries (so changing the "layout"), example:
<nick!user@host> <network> <channel>
to
<nick!user@host> <network> <channel> <amount times voiced>
... then you can do all the required changes on one place only: this set VoiceList_* matching aliases.

A similar principle applies to any form of memory used by the script.
Use prefixes for global variables, ex. %VL_system, the %VL_ part is the "prefix".
Same for hashtables, files, custom windows, timers, and so on.
This allows you to do for ex. /unset %VL_*, this will remove all global variables of your script,
and avoiding the need for huge amount /unset commands (for every variable).
This can be done in an on UNLOAD event, which is executed when people unload your script.

+++ LOGIC (CONDITIONS)
Conditions are used to execute code parts only in certain cases.
mIRC conditions have the format:
Code:

if (condition) { do something when condion is true }
else { do something when condition is false }

The symbol "!" inverts the condition:
Code:

if (!condition) { do something when condition is false }
else { do something when condition is true }

Conditions can be combined with "&&" (logical AND) or "||" (logical OR).

In the case of alot conditions, it can be very hard to see the possible logical paths the execution moves through.
This has serious implications for the maintainability of the script (to work further on it),
and it may inflict you alot extra time to figure it out over and over.
There are several ways to avoid this.
The first way is to put 'stop' conditions first.
Example:
Code:

if (condition1) {
  if (condition2) || (condition3) {
    if (condition4) { doA }
    else { doB }
  }
  else { doC }
}
else { doD }

can be rewritten as:
Code:

if (!condition1) { doD | return }
if (!condition2) && (!condition3) { doC | return }
if (!condition4) { doA }
else { doB }

Both have exactly the same logic.
Notice how the || between condition2 and condition3 became a && and the ! negating prefixes.
This strategy is called: put stop aka "bail-out" conditions first.
This makes the logic much more clear:
First all possible reasons why the code should NOT be executed, each one followed by a "stop execution-command (return or halt)".
These "bail-out" conditions usually only execute some reporting code (ex. msg $nick Sorry you don't have access | return)
After them, the script actually executes what it is intended to.

In the case of a really huge amount conditions, x levels deep on alot places, then a second approach can be used together with above one.
A typical application of this is a bot that has to react to alot !commands.
Code:

on *:TEXT:!*:#: {
  if ($1 == !command1) {
    <code to execute for !command1>
  }
  elseif ($1 == !command2) {
    <code to execute for !command2>
  }
  elseif ($1 == !command3) {
    <code to execute for !command3>
  }
  ...
  elseif ($1 == !commandN) {
    <code to execute for !commandN>
  }
  else { notice $nick Unknown !command }
}
}

With the new approach, it becomes this:
Code:

on *:TEXT:!*:#: {
  var %c = $1 | goto %c
  :!command1
  <code to execute for !command1> | return
  :!command2
  <code to execute for !command2> | return
  :!command3
  <code to execute for !command3> | return
  ...
  :!commandN
  <code to execute for !commandN> | return

  :%c | notice $nick Unknown !command | return
}

This approach allows to eliminate a whole condition level, it uses goto :labels instead.
This means that conditions inside <code to execute for !commandN> just begin as a new starting logic from the left.
It's even possible to eliminate more whole levels.
Inside <code to execute for !commandN> 'sub'-labels can be used:
Code:

on *:TEXT:!*:#: {
  var %c = $1 | goto %c

  :!command1
  var %c1 = $+(%c,_,$2) | goto %c1
  :!command1_caseA | <code to execute for caseA> | return
  :!command1_caseB | <code to execute for caseB> | return
  :%c1 | notice $nick Invalid parameter for %c | return

  :!command2
  <code to execute for !command2> | return
  :!command3
  <code to execute for !command3> | return
  ...
  :!commandN
  <code to execute for !commandN> | return

  :%c | notice $nick Unknown !command | return
}

There are more benefits with this method.
The direct return makes it faster.
When using elseif's for every command, mirc has to parse every condition till the right one, or not found.
A jump with "goto" just has to start execution on the :label.

+++ STORING DATA
mIRC provides several ways to store data. There is no 'best way in any case'.
It depends on:
- How frequent the data is accessed,
- If more data elements are needed in one operation,
- If you need the data sorted or not.

Storage ways:
- Variables.
These store one single string should be certainly shorter than 950 chars.
There are two types, "global" and "local".
Globals are created with /set /inc /dec and destroyed with /unset.
They exist until destroyed, and are stored in a .ini file.
Locals are created with /var and auto-destroyed after the current piece of code finishes execution.
It's recommended to use variable name prefixes with globals, such as %myscript.myvar1 %myscript.myvar2.
This is completely unnecessary with locals, even if you call in an event or alias another alias that uses a same-named variable, they won't interfere.

- Hash tables.
These store several strings in the form of an item-name which has a value attached.
They exist in memory and can be saved to / loaded from a diskfile.
They do not have an order.
Hashes can be very fast if they are accessed in the way they are designed for: an item name is given to get the attached value.
If you need to access all items, they lose this speed benefit.
They are well suited for settings, upon script start, you initialise or load the previously saved-hash file.
There are several methods to save them:
Either upon every change (may inflict mirc quite some disk access - slow),
or on a regular base, using a mIRC timer function and / or upon mIRC closing (on EXIT event),

- Text files.
These store $crlf terminated lines.
Since they are diskbased, they are inherently slow, but, very powerful mIRC commands are available.
When really large amounts data, this is the choice.

- INI files.
These are dedicated to settings but can also be used for list-type data.
They are organized in [sections] with under them a list of item=value lines.
Practically, they can't be seen as having an order and they are not directly sortable.
They are quite slow, even the slowest storage way.

- Custom windows.
These windows can also be used to store data, and they are memory-based so fast.
They hold their order and the powerful mIRC commands (ex. /filter) work on them.
They are easy to sort data.
You can hide them so they do not disturb the user.
Their data is lost when they close.

Of all above data storage ways, only one has an inherent structure, the INI files.
Though, structures can be simulated.
Suppose you have an INI file with as [sections] IRC channel names, and under it several channel-specific settings,
such as AutoVoice=on, !Google=on, !dns=on, and so on.
Now, we want to store this in a hashtable, for its speed benefit.
This can be achieved using the original INI [section] as prefix:
- hash item "#mychannel,!AutoVoice" and hash value "on".
- hash item "#mychannel,!Google" and hash value "on".
- hash item "#mychannel,!dns" and hash value "on".
Here, we used a comma to separate the channel prefix from the original INI item.
Why: because a comma cannot occur in a channel name. So no confusion of what is the channel name and what the item.

This introduces also a yet unmentioned but extensive mIRC command set: Token Identifiers.
A "token string" is a line of text with several substrings, called "tokens", which are separated by a specially chosen character.
In our hash item "#mychannel,!AutoVoice", this separator is the comma.
It's obvious that this separator is not allowed to occur inside the substrings (aka the "tokens").
A simple example of a token string is a list nicks, ex. nick1 nick2 nick3 nick4 nickN.
Here, a space is used as token separator character.
This space was no possible token separator for our hash, because a hashitem cannot have spaces, so a comma was chosed.

+++ SOCKETS
Sockets are communication interfaces.
If you click on mIRCs "connect" button, it will attempt a connection to an IRC server.
The IRC server has a socket open which is listening for incoming connection attempts.
This particular socket is called a "Listen port". A "port" is a property of the socket.
Since alot programs need to communicate simultaneous, portnumbers are assigned to certain types of communication.
For ex, a website that accepts HTTP connections listens on port 80.
The portnumber is a choice, an agreement between the communicating programs.
IRC servers' most used listenport is 6667.

Now, mIRC allows scripts to create connections. It provides a whole command set for it.
So it's perfectly possible to let your script connect to a website and request a page, similar to what a web browser does.
You need to know the protocol websites use, it's called HTTP.
This type of socket is called "TCP", which is a fundamental Internet low-level protocol intended to transfer data through the connection.

This is the mIRC sockets principle:
1) You execute a /sockopen <yoursocketname> <address or ip> <remote listenport>
2) mIRC executes the on SOCKOPEN event.
If the connection attempt failed, a mIRC identifier "$sockerr" has a value > 0.
If it succeeded, then the connection is established, and both sides can now send data to eachother.
The /sockwrite command is used for this.
3) When mIRC receives data, it will execute the on SOCKREAD event.
There, the /sockread command is used to store the data (or part of it) in variables.
4) When the remote side closes the connection, mIRC will execute the on SOCKCLOSE event.
If you use the /sockclose command, it will not do this, so you need to cleanup temp data same time.

Sockets can receive any data. This data can bring up certain mIRC design limitations, such as its 950 char max string length.
Webpages can easily have single lines of thousands characters.
Luckily, mIRC also provides something called "Binary variables".
These do not suffer this limit, and they also do not cause data corruption due to mIRCs internal stripping of certain characters.

Last update: 2008/01/24
Back to top
View user's profile Send private message
Display posts from previous:   
Post new topic   Reply to topic    mircscripting.info Forum Index -> Tutorials All times are GMT - 5 Hours
Page 1 of 1

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum


Powered by phpBB © 2001, 2005 phpBB Group