diff --git a/game/runner.py b/game/runner.py index 1835bcc7d0..f962d0a1c8 100644 --- a/game/runner.py +++ b/game/runner.py @@ -17,8 +17,8 @@ matter the value of this file. import os import sys from optparse import OptionParser -from subprocess import Popen, call -import Queue, thread, subprocess +from subprocess import Popen +import Queue, thread # # System Configuration @@ -276,7 +276,6 @@ def main(): if options.iportal: # make portal interactive portal_argv[1] = '--nodaemon' - PORTAL_INTERACTIVE = True set_restart_mode(PORTAL_RESTART, True) print "\nStarting Evennia Portal in non-Daemon mode (output to stdout)." else: diff --git a/src/commands/default/system.py b/src/commands/default/system.py index 52d3173948..e6d69faf9b 100644 --- a/src/commands/default/system.py +++ b/src/commands/default/system.py @@ -6,6 +6,7 @@ System commands import traceback import os, datetime, time +from sys import getsizeof import django, twisted from django.conf import settings @@ -16,9 +17,10 @@ from src.players.models import PlayerDB from src.utils import logger, utils, gametime, create from src.commands.default.muxcommand import MuxCommand +# delayed imports _resource = None _idmapper = None - +_attribute_cache = None # limit symbol import for API __all__ = ("CmdReload", "CmdReset", "CmdShutdown", "CmdPy", @@ -537,12 +539,31 @@ class CmdTime(MuxCommand): class CmdServerLoad(MuxCommand): """ - server load statistics + server load and memory statistics Usage: @serverload - Show server load statistics in a table. + This command shows server load statistics and dynamic memory + usage. + + Some Important statistics in the table: + + {wServer load{n is an average of processor usage. It's usually + between 0 (no usage) and 1 (100% usage), but may also be + temporarily higher if your computer has multiple CPU cores. + + The {wResident/Virtual memory{n displays the total memory used by + the server process. + + Evennia {wcaches{n all retrieved database entities when they are + loaded by use of the idmapper functionality. This allows Evennia + to maintain the same instances of an entity and allowing + non-persistent storage schemes. The total amount of cached objects + are displayed plus a breakdown of database object types. Finally, + {wAttributes{n are cached on-demand for speed. The total amount of + memory used for this type of cache is also displayed. + """ key = "@server" aliases = ["@serverload", "@serverprocess"] @@ -559,11 +580,13 @@ class CmdServerLoad(MuxCommand): if not utils.host_os_is('posix'): string = "Process listings are only available under Linux/Unix." else: - global _resource, _idmapper + global _resource, _idmapper, _attribute_cache if not _resource: import resource as _resource if not _idmapper: from src.utils.idmapper import base as _idmapper + if not _attribute_cache: + from src.typeclasses.models import _ATTRIBUTE_CACHE as _attribute_cache import resource loadavg = os.getloadavg() @@ -622,10 +645,10 @@ class CmdServerLoad(MuxCommand): for row in ftable: string += "\n " + "{w%s{n" % row[0] + "".join(row[1:]) - # cache size + # object cache size cachedict = _idmapper.cache_size() totcache = cachedict["_total"] - string += "\n{w Object cache usage: %5.2f MB (%i items){n" % (totcache[1], totcache[0]) + string += "\n{w Database entity (idmapper) cache usage:{n %5.2f MB (%i items)" % (totcache[1], totcache[0]) sorted_cache = sorted([(key, tup[0], tup[1]) for key, tup in cachedict.items() if key !="_total" and tup[0] > 0], key=lambda tup: tup[2], reverse=True) table = [[tup[0] for tup in sorted_cache], @@ -634,6 +657,10 @@ class CmdServerLoad(MuxCommand): ftable = utils.format_table(table, 5) for row in ftable: string += "\n " + row[0] + row[1] + row[2] + # attribute cache + size = sum([sum([getsizeof(obj) for obj in dic.values()]) for dic in _attribute_cache.values()])/1024.0 + count = sum([len(dic) for dic in _attribute_cache.values()]) + string += "\n{w On-entity Attribute cache usage:{n %5.2f MB (%i items)" % (size, count) caller.msg(string) # class CmdPs(MuxCommand): diff --git a/src/server/amp.py b/src/server/amp.py index c0c0529915..7809798c90 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -1,6 +1,7 @@ """ -Contains the protocols, commands, and client factory needed for the server -to service the MUD portal proxy. +Contains the protocols, commands, and client factory needed for the Server and Portal +to communicate with each other, letting Portal work as a proxy. Both sides use this +same protocol. The separation works like this: @@ -13,24 +14,25 @@ Server - (AMP server) Handles all mud operations. The server holds its own list and when a session connects/disconnects """ -import os +# imports needed on both server and portal side try: import cPickle as pickle except ImportError: import pickle from twisted.protocols import amp from twisted.internet import protocol -from django.conf import settings from src.utils.utils import to_str -from src.server.models import ServerConfig -from src.scripts.models import ScriptDB -from src.players.models import PlayerDB -from src.server.serversession import ServerSession - -PORTAL_RESTART = os.path.join(settings.GAME_DIR, "portal.restart") -SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart") +# these are only needed on the server side, so we delay loading of them +# so as to not have to load them on the portal too. Note: It's doubtful +# if this really matters, considering many of the +# protocols require import of django components (at least settings). +_ServerConfig = None +_ScriptDB = None +_PlayerDB = None +_ServerSession = None +_ = None #i18n hook # communication bits @@ -43,10 +45,6 @@ SDISCONNALL = chr(6) # server session disconnect all SSHUTD = chr(7) # server shutdown SSYNC = chr(8) # server session sync -# i18n -from django.utils.translation import ugettext as _ - - def get_restart_mode(restart_file): """ Parse the server/portal restart status @@ -87,7 +85,7 @@ class AmpClientFactory(protocol.ReconnectingClientFactory): """ # Initial reconnect delay in seconds. initialDelay = 1 - #factor = 1.5 + factor = 1.5 maxDelay = 1 def __init__(self, portal): @@ -115,14 +113,19 @@ class AmpClientFactory(protocol.ReconnectingClientFactory): """ Called when the AMP connection to the MUD server is lost. """ - if not get_restart_mode(SERVER_RESTART): - self.portal.sessions.announce_all(_(" Portal lost connection to Server.")) + if not hasattr(self, "server_restart_mode"): + # Don't translate this; avoiding loading django on portal side. + self.portal.sessions.announce_all(" Portal lost connection to Server.") protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason) def clientConnectionFailed(self, connector, reason): """ Called when an AMP connection attempt to the MUD server fails. """ + if hasattr(self, "server_restart_mode"): + self.maxDelay = 1 + else: + self.maxDelay = 10 self.portal.sessions.announce_all(" ...") protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) @@ -214,27 +217,27 @@ class AMPProtocol(amp.AMP): def connectionMade(self): """ This is called when a connection is established - between server and portal. It is called on both sides, + between server and portal. AMP calls it on both sides, so we need to make sure to only trigger resync from the - server side. + portal side. """ if hasattr(self.factory, "portal"): + # only the portal has the 'portal' property, so we know we are + # on the portal side and can initialize the connection. sessdata = self.factory.portal.sessions.get_all_sync_data() - #print sessdata self.call_remote_ServerAdmin(0, PSYNC, data=sessdata) - if get_restart_mode(SERVER_RESTART): - msg = _(" ... Server restarted.") - self.factory.portal.sessions.announce_all(msg) self.factory.portal.sessions.at_server_connection() + if hasattr(self.factory, "server_restart_mode"): + del self.factory.server_restart_mode # Error handling def errback(self, e, info): "error handler, to avoid dropping connections on server tracebacks." e.trap(Exception) - print _("AMP Error for %(info)s: %(e)s") % {'info': info, 'e': e.getErrorMessage()} + print "AMP Error for %(info)s: %(e)s" % {'info': info, 'e': e.getErrorMessage()} # Message definition + helper methods to call/create each message type @@ -255,7 +258,7 @@ class AMPProtocol(amp.AMP): Access method called by the Portal and executed on the Portal. """ #print "msg portal->server (portal side):", sessid, msg - self.callRemote(MsgPortal2Server, + return self.callRemote(MsgPortal2Server, sessid=sessid, msg=msg, data=dumps(data)).addErrback(self.errback, "MsgPortal2Server") @@ -276,7 +279,7 @@ class AMPProtocol(amp.AMP): Access method called by the Server and executed on the Server. """ #print "msg server->portal (server side):", sessid, msg, data - self.callRemote(MsgServer2Portal, + return self.callRemote(MsgServer2Portal, sessid=sessid, msg=to_str(msg), data=dumps(data)).addErrback(self.errback, "OOBServer2Portal") @@ -319,7 +322,7 @@ class AMPProtocol(amp.AMP): Access method called by the Server and executed on the Portal. """ #print "oob server->portal (server side):", sessid, data - self.callRemote(OOBServer2Portal, + return self.callRemote(OOBServer2Portal, sessid=sessid, data=dumps(data)).addErrback(self.errback, "OOBServer2Portal") @@ -335,14 +338,28 @@ class AMPProtocol(amp.AMP): #print "serveradmin (server side):", sessid, operation, data + # late import of django-related stuff. This avoids having to + # load these also for the portal side. + global _ServerConfig, _ScriptDB, _PlayerDB, _ServerSession, _ + if not _ServerConfig: + from src.server.models import ServerConfig as _ServerConfig + if not _ScriptDB: + from src.scripts.models import ScriptDB as _ScriptDB + if not _PlayerDB: + from src.players.models import PlayerDB as _PlayerDB + if not _ServerSession: + from src.server.serversession import ServerSession as _ServerSession + if not _: + from django.utils.translation import ugettext as _ + if operation == PCONN: #portal_session_connect # create a new session and sync it - sess = ServerSession() + sess = _ServerSession() sess.sessionhandler = self.factory.server.sessions sess.load_sync_data(data) if sess.logged_in and sess.uid: # this can happen in the case of auto-authenticating protocols like SSH - sess.player = PlayerDB.objects.get_player_from_uid(sess.uid) + sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) sess.at_sync() # this runs initialization without acr self.factory.server.sessions.portal_connect(sessid, sess) @@ -358,21 +375,22 @@ class AMPProtocol(amp.AMP): sesslist = [] server_sessionhandler = self.factory.server.sessions for sessid, sessdict in data.items(): - sess = ServerSession() + sess = _ServerSession() sess.sessionhandler = server_sessionhandler sess.load_sync_data(sessdict) if sess.uid: - sess.player = PlayerDB.objects.get_player_from_uid(sess.uid) + sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) sesslist.append(sess) # replace sessions on server server_sessionhandler.portal_session_sync(sesslist) # after sync is complete we force-validate all scripts (this starts everything) - init_mode = ServerConfig.objects.conf("server_restart_mode", default=None) - ScriptDB.objects.validate(init_mode=init_mode) - ServerConfig.objects.conf("server_restart_mode", delete=True) - + init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None) + _ScriptDB.objects.validate(init_mode=init_mode) + _ServerConfig.objects.conf("server_restart_mode", delete=True) + # let the server announce the reconnection + server_sessionhandler.announce_all(_(" ... Server restarted.")) else: - raise Exception(_("operation %(op)s not recognized.") % {'op': operation}) + raise Exception("operation %(op)s not recognized." % {'op': operation}) return {} ServerAdmin.responder(amp_server_admin) @@ -384,7 +402,7 @@ class AMPProtocol(amp.AMP): #print "serveradmin (portal side):", sessid, operation, data data = dumps(data) - self.callRemote(ServerAdmin, + return self.callRemote(ServerAdmin, sessid=sessid, operation=operation, data=data).addErrback(self.errback, "ServerAdmin") @@ -398,7 +416,7 @@ class AMPProtocol(amp.AMP): """ data = loads(data) - #print "portaladmin (portal side):", sessid, operation, data + #print "portaladmin (portal side):", sessid, ord(operation), data if operation == SLOGIN: # 'server_session_login' # a session has authenticated; sync it. sess = self.factory.portal.sessions.get_session(sessid) @@ -421,20 +439,19 @@ class AMPProtocol(amp.AMP): # it's about to shut down. We don't overwrite any sessions, # just update data on them and remove eventual ones that are # out of sync (shouldn't happen normally). - - portal_sessionhandler = self.factory.portal.sessions.sessions - + portal_sessionhandler = self.factory.portal.sessions to_save = [sessid for sessid in data if sessid in portal_sessionhandler.sessions] to_delete = [sessid for sessid in data if sessid not in to_save] - # save protocols for sessid in to_save: portal_sessionhandler.sessions[sessid].load_sync_data(data[sessid]) # disconnect missing protocols for sessid in to_delete: portal_sessionhandler.server_disconnect(sessid) + # save a flag in case connection is soon lost. + self.factory.server_restart_mode = True else: - raise Exception(_("operation %(op)s not recognized.") % {'op': operation}) + raise Exception("operation %(op)s not recognized." % {'op': operation}) return {} PortalAdmin.responder(amp_portal_admin) @@ -442,10 +459,10 @@ class AMPProtocol(amp.AMP): """ Access method called by the server side. """ - #print "portaladmin (server side):", sessid, operation, data + #print "portaladmin (server side):", sessid, ord(operation), data data = dumps(data) - self.callRemote(PortalAdmin, + return self.callRemote(PortalAdmin, sessid=sessid, operation=operation, data=data).addErrback(self.errback, "PortalAdmin") diff --git a/src/server/portal.py b/src/server/portal.py index 73b1a16f7a..885953fadd 100644 --- a/src/server/portal.py +++ b/src/server/portal.py @@ -95,7 +95,7 @@ class Portal(object): # set a callback if the server is killed abruptly, # by Ctrl-C, reboot etc. - reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _abrupt=True) + reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True) self.game_running = False @@ -139,26 +139,33 @@ class Portal(object): f.write(str(mode)) f.close() - def shutdown(self, restart=None, _abrupt=False): + def shutdown(self, restart=None, _reactor_stopping=False): """ Shuts down the server from inside it. restart - True/False sets the flags so the server will be restarted or not. If None, the current flag setting (set at initialization or previous runs) is used. - _abrupt - this is set if server is stopped by a kill command, - in which case the reactor is dead anyway. + _reactor_stopping - this is set if server is already in the process of + shutting down; in this case we don't need to stop it again. Note that restarting (regardless of the setting) will not work if the Portal is currently running in daemon mode. In that case it always needs to be restarted manually. """ + if _reactor_stopping and hasattr(self, "shutdown_complete"): + # we get here due to us calling reactor.stop below. No need + # to do the shutdown procedure again. + return self.set_restart_mode(restart) - if not _abrupt: - reactor.callLater(0, reactor.stop) if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE): # for Windows we need to remove pid files manually os.remove(PORTAL_PIDFILE) + if not _reactor_stopping: + # shutting down the reactor will trigger another signal. We set + # a flag to avoid loops. + self.shutdown_complete = True + reactor.callLater(0, reactor.stop) #------------------------------------------------------------ # diff --git a/src/server/server.py b/src/server/server.py index 8f4526f73f..73c51c052e 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -17,8 +17,7 @@ if os.name == 'nt': os.path.dirname(os.path.abspath(__file__))))) from twisted.application import internet, service -from twisted.internet import protocol, reactor, defer -from twisted.web import server, static +from twisted.internet import reactor, defer import django from django.db import connection from django.conf import settings @@ -106,7 +105,7 @@ class Evennia(object): # set a callback if the server is killed abruptly, # by Ctrl-C, reboot etc. - reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _abrupt=True) + reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True) self.game_running = True @@ -133,6 +132,7 @@ class Evennia(object): """ This attempts to run the initial_setup script of the server. It returns if this is not the first time the server starts. + Once finished the last_initial_setup_step is set to -1. """ last_initial_setup_step = ServerConfig.objects.conf('last_initial_setup_step') if not last_initial_setup_step: @@ -184,10 +184,12 @@ class Evennia(object): returned so the server knows which more it's in. """ if mode == None: - if os.path.exists(SERVER_RESTART) and 'True' == open(SERVER_RESTART, 'r').read(): + f = open(SERVER_RESTART, 'r') + if os.path.exists(SERVER_RESTART) and 'True' == f.read(): mode = 'reload' else: mode = 'shutdown' + f.close() else: restart = mode in ('reload', 'reset') f = open(SERVER_RESTART, 'w') @@ -195,7 +197,8 @@ class Evennia(object): f.close() return mode - def shutdown(self, mode=None, _abrupt=False): + @defer.inlineCallbacks + def shutdown(self, mode=None, _reactor_stopping=False): """ Shuts down the server from inside it. @@ -204,11 +207,15 @@ class Evennia(object): 'reset' - server restarts, non-persistent scripts stopped, at_shutdown hooks called. 'shutdown' - like reset, but server will not auto-restart. None - keep currently set flag from flag file. - _abrupt - this is set if server is stopped by a kill command, - in which case the reactor is dead anyway. + _reactor_stopping - this is set if server is stopped by a kill command OR this method was already called + once - in both cases the reactor is dead/stopping already. """ - mode = self.set_restart_mode(mode) + if _reactor_stopping and hasattr(self, "shutdown_complete"): + # this means we have already passed through this method once; we don't need + # to run the shutdown procedure again. + defer.returnValue(None) + mode = self.set_restart_mode(mode) # call shutdown hooks on all cached objects from src.objects.models import ObjectDB @@ -217,31 +224,34 @@ class Evennia(object): if mode == 'reload': # call restart hooks - [(o.typeclass, o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()] - [(p.typeclass, p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()] - [(s.typeclass, s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()] - + yield [(o.typeclass, o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()] + yield [(p.typeclass, p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()] + yield [(s.typeclass, s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()] + yield self.sessions.all_sessions_portal_sync() ServerConfig.objects.conf("server_restart_mode", "reload") - else: if mode == 'reset': # don't call disconnect hooks on reset - [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()] + yield [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()] + yield self.all_sessions_portal_sync() else: # shutdown - [(o.typeclass, o.at_disconnect(), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()] + yield [(o.typeclass, o.at_disconnect(), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()] - [(p.typeclass, p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()] - [(s.typeclass, s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()] + yield [(p.typeclass, p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()] + yield [(s.typeclass, s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()] ServerConfig.objects.conf("server_restart_mode", "reset") - if not _abrupt: - if SERVER_HOOK_MODULE: - SERVER_HOOK_MODULE.at_server_stop() - reactor.callLater(0, reactor.stop) + if SERVER_HOOK_MODULE: + SERVER_HOOK_MODULE.at_server_stop() + # if _reactor_stopping is true, reactor does not need to be stopped again. if os.name == 'nt' and os.path.exists(SERVER_PIDFILE): # for Windows we need to remove pid files manually os.remove(SERVER_PIDFILE) + if not _reactor_stopping: + # this will also send a reactor.stop signal, so we set a flag to avoid loops. + self.shutdown_complete = True + reactor.callLater(0, reactor.stop) #------------------------------------------------------------ # diff --git a/src/server/session.py b/src/server/session.py index 9fe624096e..d76cc0c821 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -33,10 +33,10 @@ class Session(object): """ # names of attributes that should be affected by syncing. - _attrs_to_sync = ['protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname', + _attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname', 'logged_in', 'cid', 'encoding', 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', - 'server_data'] + 'server_data') def init_session(self, protocol_key, address, sessionhandler): """ @@ -84,18 +84,15 @@ class Session(object): """ Return all data relevant to sync the session """ - sessdata = {} - for attrname in self._attrs_to_sync: - sessdata[attrname] = self.__dict__.get(attrname, None) - return sessdata + return dict((key, value) for key, value in self.__dict__.items() if key in self._attrs_to_sync) def load_sync_data(self, sessdata): """ Takes a session dictionary, as created by get_sync_data, - and loads it into the correct attributes of the session. + and loads it into the correct properties of the session. """ - for attrname, value in sessdata.items(): - self.__dict__[attrname] = value + for propname, value in sessdata.items(): + self.__dict__[propname] = value def at_sync(self): """ diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index ec36935c1a..bba1607d22 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -69,11 +69,7 @@ class SessionHandler(object): Create a dictionary of sessdata dicts representing all sessions in store. """ - sessdict = {} - for sess in self.sessions.values(): - # copy all relevant data from all sessions - sessdict[sess.sessid] = sess.get_sync_data() - return sessdict + return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items()) #------------------------------------------------------------ # Server-SessionHandler class @@ -179,14 +175,14 @@ class ServerSessionHandler(SessionHandler): operation=SLOGIN, data=sessdata) - def session_sync(self): + def all_sessions_portal_sync(self): """ This is called by the server when it reboots. It syncs all session data - to the portal. + to the portal. Returns a deferred! """ sessdata = self.get_all_sync_data() - self.server.amp_protocol.call_remote_PortalAdmin(0, - SSYNC, + return self.server.amp_protocol.call_remote_PortalAdmin(0, + operation=SSYNC, data=sessdata) diff --git a/src/utils/dummyrunner_actions.py b/src/utils/dummyrunner_actions.py index 854d8359b0..6343a46712 100644 --- a/src/utils/dummyrunner_actions.py +++ b/src/utils/dummyrunner_actions.py @@ -144,15 +144,15 @@ def c_moves(client): # # heavy builder definition -ACTIONS = ( c_login, - c_logout, - (0.2, c_looks), - (0.1, c_examines), - (0.2, c_help), - (0.1, c_digs), - (0.1, c_creates_obj), - #(0.1, c_creates_button), - (0.2, c_moves)) +#ACTIONS = ( c_login, +# c_logout, +# (0.2, c_looks), +# (0.1, c_examines), +# (0.2, c_help), +# (0.1, c_digs), +# (0.1, c_creates_obj), +# #(0.1, c_creates_button), +# (0.2, c_moves)) # "normal builder" definition #ACTIONS = ( c_login, # c_logout, @@ -164,13 +164,13 @@ ACTIONS = ( c_login, # #(0.1, c_creates_button), # (0.3, c_moves)) # "normal player" definition -#ACTIONS = ( c_login, -# c_logout, -# (0.4, c_looks), -# #(0.1, c_examines), -# (0.2, c_help), -# #(0.1, c_digs), -# #(0.1, c_creates_obj), -# #(0.1, c_creates_button), -# (0.4, c_moves)) +ACTIONS = ( c_login, + c_logout, + (0.7, c_looks), + #(0.1, c_examines), + (0.3, c_help)) + #(0.1, c_digs), + #(0.1, c_creates_obj), + #(0.1, c_creates_button), + #(0.4, c_moves))