99 simple asynchronous bottles on the wall
Here are some things that may help you improve your code.

Don't use usleep()

The usleep() function was part of the POSIX standard, but it was marked obsolete in 2001 and removed entirely from the 2009 version of the POSIX standard.  You can use nanosleep instead.

Make it clear when loops end

The code currently uses while (1) in main, but it doesn't really loop infinitely.  It's better, generally, to have the loop exit condition explicitly stated.  For example we could rewrite your main() loop:

while (events_head.next) {
    update_time();
    check_events();
    usleep(900000);
}


This makes it much more clear when and why the loop will end.

Return something useful from the subroutines

Many of the functions that claim to retun an int don't return anything at all, so they should be declared void instead.  Alternatively, if there's a value that could usefully be returned, modify the functions to return that instead.

Check return values and handle errors

The code calls malloc but never checks for error return values.  This is a serious problem that must be addressed.  If the sytem ever runs out of memory, the program will crash.

Consider re-using memory items

Each time an event is registered, new memory is allocated, and each time an event is processed, that memory is freed, even when, as in the case of event_99_bottles, the task re-registers itself.  One way to do this would be to have a reregister_event call.

Use a void * as the argument

Right now, if you decide to create a new type of event which uses a new type of argument, you'll have to recompile the whole code.  If instead of the EventArugment structure, the EventList contained a void *, then that part of the code would be able to remain the same, even if more elaborate data needed to be passed to specific events.  It would then be up to the called function to cast this to whatever it was expecting as input.

Consider using a priority queue

What you seem to be working toward is something like the event dispatcher for a generic task scheduling routine as might be used in a simple operating system.  Such task schedulers often use a priority queue to allow for higher and lower priority tasks.

Consider using an intelligent wait

The check_events() routine doesn't currently return anything useful, but it could.  If it traversed the list, checking for the next deadline and returned that time, then the code could simply sleep for that amount of time, very efficiently only waking up when there was work to do.

Eliminate global variables where practical

The code currently uses two global variables (current_time and events_head) that probably don't really need to be global.  Better would be to pass those as arguments to the functions that need them and/or putting them both inside an Events structure.  If you decide to keep either those (or an Events structure) outside of any function, consider making them static which limits them to file scope.  Generally speaking, it's good practice to make the scope of variables as small as practical.