indexing description: "[ Base class for network peers in a multiplayer environment. This class is the core of the EM multiplayer framework and implements functionality that is not bound to a specific peer architecture (like peer-to-peer or client-server). EM_NET_BASE is a generic class. You need to specify a generic parameter that is a subclass of EM_NET_OBJECT_TYPES in which you register your own network objects. See EM_NET_OBJECT_TYPES for further details about how to register your own objects. It is very important that each peer, whose implementation is not necessarily identical to remote peers, uses the same concrete EM_NET_OBJECT_TYPES. In other words: The (concrete) generic type of every EM_NET_BASE in a network must conform with all other participants. EM_NET_BASE is itself an EM_UDP_SOCKET which is used for data sending and receiving over a UDP socket. There are currently two implementations of EM_NET_BASE: - EM_NET_SERVER - EM_NET_CLIENT Together they build a client-server architecture which is usually used for network games. For further explanations about the application of those classes have a look at the corresponding class description. Note: It is probably not necessary to make (any?) changes to EM_NET_BASE if you want to use your own network architecture. For example, it is possible to create a peer to peer system just by extending EM_NET_BASE. ]" date: "$Date$" revision: "$Revision$" deferred class EM_NET_BASE [TYPES -> EM_NET_OBJECT_TYPES create make_and_initialize_factory end] inherit EM_UDP_SOCKET rename make as make_udp_socket export {NONE} end EM_DELAYED_PROCEDURES rename make as make_delayed_procedures, time as timer -- export --TODO: check who would need what features to minimize the API... -- {EM_UDP_SOCKET_HANDLER, EM_MP_CLIENT_SCENE, EM_MP_SERVER_SCENE} all end EM_NET_EVENT_PROCESSOR rename publish as local_publish, make as make_event_processor end EM_TIME_SINGLETON rename time as timer export {NONE} all end feature {EM_NET_SERVER, EM_NET_CLIENT} -- Initialization make(a_port: INTEGER) is -- Initialisation require port_is_valid: a_port >= 0 and a_port <= 65535 do -- Precursors make_event_processor make_delayed_procedures -- Creation and initialisation. create protocol.make create objects.make(256) create net_object_factory.make create object_types.make_and_initialize_factory (net_object_factory) create connections.make(20) create dynamic_connection_list.make create timeout_event create pending_2pc_acks.make (16) create id_manager.make create event_2pc_id_manager.make event_container := object_types.create_em_net_event_container_object event_container.set_net_object_factory(net_object_factory) -- Socket setup make_udp_socket set_port(a_port) open -- Group setup initialize_groups -- Protocol setup protocol.set_time_sync_group (standard_group) subscribe_by_type_id (object_types.em_net_time_sync_request, agent protocol.on_time_sync_request (?)) subscribe_by_type_id (object_types.em_net_time_sync_response, agent protocol.on_time_sync_response (?)) subscribe_by_type_id (object_types.em_net_2pc_ack, agent on_2pc_ack (?)) -- Packet and eventhandling data_received_event.subscribe (agent on_packet_received (?)) protocol.set_object_update_handler (agent on_object_update_received) add_object_internal(event_container) -- Auto transmit enabled by default (scheiss auto transmit :) net_fps := 10 ensure then protocol_created: protocol /= Void object_types_created: object_types /= Void objects_created: objects /= Void object_types_created: object_types /= Void connections_created: connections /= Void dynamic_connection_list_created: dynamic_connection_list /= Void timeout_event_created: timeout_event /= Void groups_created: groups /= Void id_manager_created: id_manager /= Void event_2pc_id_manager_created: event_2pc_id_manager /= Void end feature -- ID management net_object_factory: EM_NET_OBJECT_FACTORY -- Object creation factory id_manager: EM_NET_OBJECT_ID_MANAGER -- ID manager for net objects event_2pc_id_manager: EM_NET_OBJECT_ID_MANAGER -- ID manager for events feature -- Network enable_auto_transmit is -- Enable auto transmission of packets. -- The mechanism tries to send packets with a frequency of `net_fps'. require not_enabled: not is_auto_transmit_enabled do is_auto_transmit_enabled := True timer.add_timed_procedure (agent on_auto_transmit, 0) ensure auto_transmit_enabled: is_auto_transmit_enabled end disable_auto_transmit is -- Disable auto transmission. require enabled: is_auto_transmit_enabled do is_auto_transmit_enabled := False ensure auto_transmit_disabled: not is_auto_transmit_enabled end send_update is -- Send updated objects for each group to clients. local a_connection_list: DS_LINKED_LIST[EM_NET_CONNECTION] all_objects: DS_LINKED_LIST[EM_NET_OBJECT] objects_to_sync: DS_LINKED_LIST[EM_NET_OBJECT] output_stream : EM_NET_SERIALIZER do check_for_timeouts create objects_to_sync.make create output_stream.make_empty from groups.start until groups.after loop all_objects := groups.item_for_iteration.objects a_connection_list := groups.item_for_iteration.connections objects_to_sync.wipe_out from all_objects.start until all_objects.after loop if all_objects.item_for_iteration.is_synchronisation_needed then objects_to_sync.put_first (all_objects.item_for_iteration) end all_objects.forth end from until objects_to_sync.is_empty loop protocol.create_packet (objects_to_sync, output_stream) from a_connection_list.start until a_connection_list.after loop protocol.last_packet.set_address (a_connection_list.item_for_iteration.socket_address) send (protocol.last_packet) a_connection_list.forth end end groups.forth end end feature -- Event handling publish (an_event: EM_NET_EVENT_OBJECT) is -- Publish `an_event' to the standard group. require an_event_not_void: an_event /= Void do an_event.set_group (standard_group) an_event.publish end timeout_event: EM_EVENT_CHANNEL[TUPLE[EM_NET_CONNECTION]] -- Event that is called if a connection timeout occurs feature -- Connection handling create_connection (a_socket_address: EM_INET_SOCKET_ADDRESS) is -- Create a connection and monitor it for timeouts. require a_socket_address_not_void: a_socket_address /= Void do create_static_connection (a_socket_address) dynamic_connection_list.put_first (last_created_connection) end create_static_connection (a_socket_address: EM_INET_SOCKET_ADDRESS) is -- Create a connection and don't monitor it for timeouts. -- Use this for broadcasts and other cases where you have mainly outgoing traffic. require a_socket_address_not_void: a_socket_address /= Void local a_socket_address_hexstring: STRING do create last_created_connection.make -- TODO: implement non blocking resolve if not a_socket_address.is_resolved then a_socket_address.resolve_blocking end a_socket_address_hexstring := a_socket_address.to_hex_string create_group (a_socket_address_hexstring) last_created_connection.join (last_created_group) last_created_connection.set_personal_group (last_created_group) last_created_connection.set_socket_address (a_socket_address) connections.force (last_created_connection, a_socket_address_hexstring) end delete_connection (a_connection: EM_NET_CONNECTION) is -- Delete `a_connection'. require a_connection_not_void: a_connection /= Void do a_connection.leave_all_groups -- a_connection.personal_group. connections.remove (a_connection.personal_group.name) remove_group (a_connection.personal_group) dynamic_connection_list.delete (a_connection) ensure connection_removed: not connections.has_item (a_connection) connection_from_dynamic_list_removed: not dynamic_connection_list.has (a_connection) end last_created_connection: EM_NET_CONNECTION -- Last created connection feature -- Time synchronisation enable_time_sync is -- Enable time synchronisation. do protocol.enable_time_sync end disable_time_sync is -- Disable time synchronisation. do protocol.disable_time_sync end is_time_sync_enabled: BOOLEAN is -- Is time synchronisation enabled? do Result := protocol.is_time_sync_enabled end set_time_sync_group (a_group: EM_NET_GROUP) is -- Set `time_sync_group' to `a_group'. require a_group_not_void: a_group /= Void do protocol.set_time_sync_group (a_group) ensure protocol_group_updated: protocol.time_sync_group = time_sync_group end time_sync_group: EM_NET_GROUP is -- Group used for time synchronisation do Result := protocol.time_sync_group end time: INTEGER is -- Time in milliseconds! do Result := protocol.time end feature -- Object management add_object (an_object: EM_NET_OBJECT) is -- Add `an_object' to the standard group. require an_object_not_void: an_object /= Void do standard_group.add_object (an_object) ensure added_to_standard_group: standard_group.has_object (an_object) end remove_object (an_object: EM_NET_OBJECT) is -- Remove `an_object' from the standard group. require an_object_not_void: an_object /= Void standard_group_has_object: standard_group.has_object (an_object) do standard_group.remove_object (an_object) ensure removed_from_standard_group: not standard_group.has_object (an_object) end has_object (an_object: EM_NET_OBJECT) : BOOLEAN is -- Does the standard group contain `an_object'? require an_object_not_void: an_object /= Void do Result := objects.has (an_object.id) check object_id_identifies_same_object: an_object = objects.item (an_object.id) end end feature -- Group management create_group (a_group_name: STRING) is -- Create a new group with `a_group_name' and make it available in `last_created_group'. -- `a_group_name' must be unique. require group_not_exist: not group_exists_by_name (a_group_name) local a_group: EM_NET_GROUP do create a_group.make (a_group_name, me) last_created_group := a_group end remove_group_by_name(a_group_name: STRING) is -- Remove group with `a_group_name'. require group_exists_by_name: group_exists_by_name(a_group_name) local a_group: EM_NET_GROUP do a_group := groups.item (a_group_name) groups.remove (a_group_name) ensure group_deleted: not group_exists_by_name(a_group_name) end remove_group (a_group: EM_NET_GROUP) is -- Remove `a_group'. require group_exists: group_exists(a_group) do check group_name_identifies_same_group: a_group = group(a_group.name) end remove_group_by_name (a_group.name) ensure group_deleted: not group_exists(a_group) end last_created_group: EM_NET_GROUP -- Last group that was created by `create_group' group_exists_by_name (a_group_name: STRING): BOOLEAN is -- Does group with `a_group_name' exist? require group_name_not_void: a_group_name /= Void do Result := groups.has(a_group_name) end group_exists (a_group: EM_NET_GROUP): BOOLEAN is -- Does `a_group' exist? require group_not_void: a_group /= Void do Result := groups.has_item (a_group) end group (a_group_name: STRING): EM_NET_GROUP is -- Group with `group_name' require group_name_not_void: a_group_name /= Void group_exists_by_name: group_exists_by_name (a_group_name) do Result := groups.item (a_group_name) end set_standard_group(a_group_name: STRING) is -- Set `standard_group' to the group with `a_group_name'. require group_name_not_void: a_group_name /= Void group_exists_by_name: group_exists_by_name (a_group_name) do standard_group := group(a_group_name) ensure standard_group_set: standard_group = group(a_group_name) end standard_group: EM_NET_GROUP -- Standard group -- Many convenience features use this group feature -- Element change set_net_fps (a_net_fps_value: INTEGER) is -- Set `net_fps' to `a_net_fps_value'. require positive_net_fps_value: a_net_fps_value > 0 do net_fps := a_net_fps_value ensure net_fps_set: net_fps = a_net_fps_value end feature -- Status information is_auto_transmit_enabled: BOOLEAN -- Is auto transmit enabled? -- Use this to automate the sending of outgoing data. -- This relies on the `net_fps' frequency. net_fps: INTEGER -- Network updates per second game_identifier: STRING -- Game identifier object_types: TYPES -- Object types which can be used in this implementation feature {EM_NET_GROUP} -- Internal object management implementation add_object_internal (an_object: EM_NET_OBJECT) is -- Add `an_object' to the list of synchronized objects. require object_not_void: an_object /= Void object_not_yet_added: not has_object (an_object) do -- dem objekt muss eine im ganzen netzwerk eindeutige ID gegeben werden objects.force (an_object, an_object.id) ensure object_added: has_object(an_object) end remove_object_internal (an_object: EM_NET_OBJECT) is -- Remove `an_object' from the list of synchronized objects. require object_not_void: an_object /= Void has_object: has_object(an_object) do check object_id_identifies_same_object: an_object = objects.item (an_object.id) end objects.remove (an_object.id) ensure object_removed: not has_object (an_object) end feature {EM_NET_GROUP} -- Group implementation groups: DS_HASH_TABLE[EM_NET_GROUP, STRING] -- List of groups objects: DS_HASH_TABLE[EM_NET_OBJECT,INTEGER] -- List of objects which are synchronized over the network updating_connection: EM_NET_CONNECTION -- Connection which sent the last update feature {EM_NET_2PC_EVENT_OBJECT} -- Two phase commit pending_2pc_acks: DS_HASH_TABLE[EM_PAIR[EM_NET_2PC_EVENT_OBJECT,DS_LINKED_LIST[EM_NET_CONNECTION]],INTEGER] -- List of pending 2PC events -- The key of the hashtable is the `event_id'. -- The value of the hashtable is the EVENT itself and the list of connections whose ACKs are not received yet. enable_2pc_timed_resend (an_event: EM_NET_2PC_EVENT_OBJECT) is -- Enable timed resend of `an_event'. require event_not_void: an_event /= Void do debug ("em_mp") io.put_string ("Scheduled callback in " + an_event.resend_time.out + " milliseconds to resend event with ID " + an_event.id.out + ".%N") end timer.add_timed_procedure (agent resend_2pc (an_event), an_event.resend_time) end resend_2pc (an_event: EM_NET_2PC_EVENT_OBJECT) is -- Send `an_event' to clients that have not sent an acknowledgement yet. require event_not_void: an_event /= Void local connection_list: DS_LINKED_LIST[EM_NET_CONNECTION] connection_cursor: DS_LINKED_LIST_CURSOR[EM_NET_CONNECTION] event_id: INTEGER do event_id := an_event.id debug ("em_mp") io.put_string ("A timed callback to resend event with ID " + an_event.id.out + " has been started.%N") end if pending_2pc_acks.has (event_id) then connection_list := pending_2pc_acks.item (event_id).second if an_event.resends_left = 0 and then connection_list.count > 0 then debug ("em_mp") io.put_string ("Maximum resend count of event with ID " + an_event.id.out + " has been reached. Removing it and call timeout agent.%N") end if an_event.timeout_agent /= Void then an_event.timeout_agent.call ([connection_list]) end pending_2pc_acks.remove (event_id) else debug ("em_mp") io.put_string ("Resending event with ID " + an_event.id.out + ".%N") end if connection_list.count > 0 then create connection_cursor.make (connection_list) from connection_cursor.start until connection_cursor.after loop an_event.set_group (connection_cursor.item.personal_group) an_event.publish connection_cursor.forth end an_event.set_resends_left (an_event.resends_left - 1) enable_2pc_timed_resend (an_event) end end else debug ("em_mp") io.put_string ("Skipped resend of event with ID " + an_event.id.out + " because it is not pending anymore (=consensus reached).%N") end end end on_2pc_ack(an_ack: EM_NET_2PC_ACK) is -- Handle client ACK messages. require ack_not_void: an_ack /= Void local pending_list: DS_LINKED_LIST[EM_NET_CONNECTION] event: EM_NET_2PC_EVENT_OBJECT do if pending_2pc_acks.has (an_ack.event_id) then debug ("em_mp") io.put_string ("Received acknowledgement for event with ID " + an_ack.event_id.out + " from connection " + an_ack.updating_connection.personal_group.name + ".%N") end pending_list := pending_2pc_acks.item (an_ack.event_id).second pending_list.delete (an_ack.updating_connection) if pending_list.count = 0 then event := pending_2pc_acks.item (an_ack.event_id).first if event.success_agent /= Void then event.success_agent.call ([]) end pending_2pc_acks.remove (an_ack.event_id) end else debug ("em_mp") io.put_string ("Received an acknowledgement, but event with ID " + an_ack.event_id.out + " is already acknowledged.%N") end end ensure event_not_pending_for_connection: not pending_2pc_acks.has (an_ack.event_id) or else not pending_2pc_acks.item (an_ack.event_id).second.has (an_ack.updating_connection) end feature {NONE} -- Network implementation on_new_connection(a_socket_address: EM_INET_SOCKET_ADDRESS) is -- Event called if a sofar unkonwn socket has sent a packet. deferred end on_packet_received (a_packet: EM_UDP_PACKET) is -- Event called if a packet was received. local socket_address: EM_INET_SOCKET_ADDRESS socket_hex_string: STRING a_connection: EM_NET_CONNECTION do socket_address := a_packet.address socket_hex_string := socket_address.to_hex_string if not connections.has (socket_hex_string) then -- TODO: Performance could be increased by using EM_POOL on_new_connection (socket_address) end if connections.has (socket_hex_string) then a_connection := connections.item (socket_hex_string) a_connection.set_last_update (timer.ticks) updating_connection := a_connection check updating_connection.socket_address /= Void end protocol.parse_packet (a_packet) -- Process possibly arrived events. distribute_arrived_events end end distribute_arrived_events is -- Distribute arrived events. local cursor: DS_LINKED_LIST_CURSOR[EM_NET_EVENT_OBJECT] do -- Event handling and distribution. from create cursor.make (event_container.incoming_net_events) cursor.start until cursor.after loop local_publish (cursor.item) cursor.item.updating_connection.publish (cursor.item) cursor.forth end event_container.wipe_out end on_object_update_received (a_time: INTEGER; an_object_id: INTEGER; a_deserializer: EM_NET_UNSERIALIZER) is -- Event that is called if a new serialized object arrived. -- This will check the object list and update the objects data. -- `time' is the timestamp when the packet was created on the remote side. -- `object_id' is the ID of the EM_NET_OBJECT -- `deserializer' is the stream that contains the serialized object. Note that -- other objects might be included in this stream therefore you must not modify it. -- The cursor in the stream points to the beginning of the current object. local an_object : EM_NET_OBJECT do if objects.has (an_object_id) then an_object := objects.item (an_object_id) an_object.set_updating_connection (updating_connection) check updating_connection /= Void end an_object.set_time_offset (time - a_time) an_object.unserialize (a_deserializer) else error_handler.raise_error (error_handler.em_error_net_object_missed,[an_object_id]) end end check_for_timeouts is -- Check timeouts for each group. local a_connection: EM_NET_CONNECTION a_cursor: DS_LINKED_LIST_CURSOR[EM_NET_CONNECTION] do from create a_cursor.make (dynamic_connection_list) a_cursor.start until a_cursor.after loop a_connection := a_cursor.item a_cursor.forth if a_connection.last_receive_time + a_connection.max_idle_time < timer.ticks then timeout_event.publish([a_connection]) end end end protocol: EM_NET_PROTOCOL -- Protocol used to send and receive messages event_container: EM_NET_EVENT_CONTAINER_OBJECT -- Container that wrapps multiple events -- Use event_container of groups to send out events. initialize_groups is -- Initialize groups and create some default groups. do create groups.make_default create_group ("STD") standard_group := last_created_group end on_auto_transmit is -- Perform a transmition. do if is_auto_transmit_enabled then send_update timer.add_timed_procedure (agent on_auto_transmit, 1000//net_fps) end end me: EM_NET_BASE[EM_NET_OBJECT_TYPES] is -- Reference to Current casted to the most basic type. do Result ?= Current check me_not_void: me /= Void end end connections: DS_HASH_TABLE[EM_NET_CONNECTION,STRING] -- List of connections dynamic_connection_list: DS_LINKED_LIST[EM_NET_CONNECTION] -- List of dynamic connections -- Implementation of timeout monitoring. invariant event_container_correct: event_container = objects.item (object_types.em_net_event_container_object) end