indexing description: "[ Highscore class. Highscore is stored locally. You can add and update entries, sort and crop the highscore. ]" date: "$Date$" revision: "$Revision$" class EM_LOCAL_HIGHSCORE inherit XM_CALLBACKS_FILTER_FACTORY export {NONE} all end EM_TIME_SINGLETON export {NONE} all end EM_SHARED_USER_DIRECTORY export {NONE} all end EM_SHARED_APPLICATION_ID create make, make_extended feature -- Initialization make is -- create highscore require application_id_set: application_id.item /= void do make_extended (<<>>) end make_extended (the_other_keys: ARRAY [STRING]) is -- create highscore with more keys require no_keywords: not has_keywords(the_other_keys) the_other_keys /= void application_id_set: application_id.item /= void do set_local_file (user_directory.item, application_id.item + "_highscore.xml") create other_keys.make_from_array (the_other_keys) other_keys.compare_objects num_of_local_entries := 10 create local_highscore.make create checksum_generator create highscore_checksum_filter.make_null end feature -- Element change set_num_of_local_entries (a_num_of_local_entries: like num_of_local_entries) is -- Set `num_of_local_entries' to `a_num_of_local_entries'. do num_of_local_entries := a_num_of_local_entries ensure num_of_local_entries_assigned: num_of_local_entries = a_num_of_local_entries end set_local_file (a_directory: STRING; a_filename: STRING) is -- set path for highscore file require a_filename /= void a_directory.item (a_directory.count) = '/' do create highscore_dir.make (a_directory) create highscore_file.make (a_directory + a_filename) end feature -- Read/Write read_local is -- read highscore from a local file require file_set: highscore_file /= void do parse_highscore_file (highscore_file) end write_local is -- write local highscore to disc require highscore_file_set: highscore_file /= void do checksum_generator.reset if not highscore_file.exists then -- todo: i want to create the file, not open it yet if not highscore_dir.exists then highscore_dir.recursive_create_directory end if highscore_file.is_creatable then highscore_file.create_read_write highscore_file.close end end if highscore_file.exists and then highscore_file.is_writable then highscore_file.open_write -- todo -- Write out all elements in highscore table highscore_file.put_string ("%N%N") highscore_file.put_string ("<" + xml_section + ">%N") from local_highscore.start until local_highscore.after or local_highscore.index > num_of_local_entries loop highscore_file.put_string ("%T%N") local_highscore.forth end checksum_generator.append_string (client_id.out) highscore_file.put_string ("%T%N") checksum_generator.generate_checksum highscore_file.put_string ("%T%N") highscore_file.put_string ("%N") highscore_file.close else debug io.put_string ("Writing highcore file failed%N") end end end feature {NONE} -- Read/Write Implementation parse_highscore_file (a_file: like highscore_file) is -- read highscore entries from a local file if they are valid require file_set: a_file /= void do create highscore_parser.make create highscore_xm_filter.make_null highscore_parser.set_callbacks (callbacks_pipe (<>)) if a_file /= void and then a_file.exists and then a_file.is_readable then a_file.open_read a_file.read_stream (a_file.count) highscore_parser.parse_from_string (a_file.last_string) a_file.close end -- if file was parsed and the checksum is correct, integrate it into local if highscore_xm_filter.highscore /= void and then highscore_checksum_filter.checksum = highscore_xm_filter.checksum then client_id := highscore_xm_filter.client_id create local_highscore.make_default from highscore_xm_filter.highscore.start until highscore_xm_filter.highscore.after loop add_entry_from_table (highscore_xm_filter.highscore.item_for_iteration) highscore_xm_filter.highscore.forth end else debug io.put_string ("Could not read file.%N") io.put_string ("That most probably means the file either didn't exist or the checksum is incorrect%N") end end if client_id = 0 then client_id := generate_client_id end sort_local_by_score (false) highscore_parser := void highscore_xm_filter := void end feature -- Add/Update entries add_or_update_entry (a_name: STRING; a_score: INTEGER) is -- add or update entry depending on if it's already present or not require name_valid: is_valid_string (a_name) do add_or_update_entry_extended (a_name, a_score, <<>>) end add_or_update_entry_extended (a_name: STRING; a_score: INTEGER; other_values: ARRAY[STRING]) is -- add or update extended entry depending of it's already present or not require name_valid: is_valid_string (a_name) right_number_of_values: other_values.count = other_keys.count do if has_entry (a_name) and is_better (a_name, a_score) then update_entry_extended (a_name, a_score, other_values) else add_entry_extended (a_name, a_score, other_values) end end add_entry (a_name: STRING; a_score: INTEGER) is -- add entry to local highscore require name_valid: is_valid_string (a_name) do add_entry_extended (a_name, a_score, <<>>) end add_entry_extended (a_name: STRING; a_score: INTEGER; other_values: ARRAY[STRING]) is -- add extended entry to local highscore require name_valid: is_valid_string (a_name) right_number_of_values: other_values.count = other_keys.count local i: INTEGER new_entry: EM_HIGHSCORE_ENTRY do -- todo: here I allocate for 'keys.count', but get keys.count + 1 entries in the table? create new_entry.make (other_keys.count) new_entry.set_name (a_name) new_entry.set_score (a_score) from i := 1 until i > other_values.count loop new_entry.put (other_values.item (i), other_keys.item(i)) i := i + 1 end local_highscore.put_last (new_entry) end update_entry (the_name: STRING; the_score: INTEGER) is -- update entry in local highscore require name_valid: is_valid_string (the_name) do update_entry_extended (the_name, the_score, <<>>) end update_entry_extended (the_name: STRING; a_score: INTEGER; other_values: ARRAY[STRING]) is -- update extended in local highscore require name_valid: is_valid_string (the_name) right_number_of_values: other_values.count = other_keys.count local updated: BOOLEAN the_entry: like highscore_entries i: INTEGER do -- then update this value in the table from local_highscore.start until local_highscore.after or updated loop if local_highscore.item_for_iteration.name.is_equal (the_name) then the_entry := local_highscore.item_for_iteration the_entry.set_name (the_name) the_entry.set_score (a_score) from i := 1 until i > other_keys.count loop the_entry.replace (other_values.item (i), other_keys.item (i)) i := i + 1 end updated := true end local_highscore.forth end end has_entry (a_name: STRING): BOOLEAN is -- check if name is in local highscore require name_not_void: a_name /= Void do from local_highscore.start until local_highscore.after or Result = true loop -- todo Result := local_highscore.item_for_iteration.name.is_equal (a_name) local_highscore.forth end end is_better (a_name: STRING; a_score: INTEGER): BOOLEAN is -- check if 'a_score' of 'a_name' is better than the one already in highscore require has_entry (a_name) do from local_highscore.start until local_highscore.after or Result = true loop if local_highscore.item_for_iteration.name.is_equal (a_name) then Result := a_score > local_highscore.item_for_iteration.score end local_highscore.forth end end is_valid_string (a_string: STRING): BOOLEAN is -- check if string is valid (doesn't contain special characters local i: INTEGER do Result := (a_string /= void) and then (a_string.count > 0) from i := 1 until i > a_string.count or Result = false loop Result := Result and is_valid_character(a_string.item (i)) i := i + 1 end end is_valid_character (a_char: CHARACTER): BOOLEAN is -- check if character is valid (isn't special character) do Result := a_char.is_alpha or a_char.is_digit or a_char.is_equal ('_') end feature {NONE} -- Add entry Implementation add_entry_from_table (the_entry: DS_HASH_TABLE[STRING, STRING]) is -- add en entry from a table generated by the xml parser require entry_not_void: the_entry /= void local the_values: ARRAY[STRING] has_errors: BOOLEAN i: INTEGER do if not the_entry.has (name) then has_errors := true end if not the_entry.has (score) and then not the_entry.item (score).is_integer then has_errors := true end create the_values.make(1, other_keys.count) from i := 1 until i > other_keys.count or has_errors loop if the_entry.has (other_keys.item (i)) then the_values.put (the_entry.item (other_keys.item (i)), i) else the_values.put ("", i) end i := i + 1 end if not has_errors then add_entry_extended (the_entry.item (name), the_entry.item (score).to_integer, the_values) end end feature -- Sort and Crop crop_local (nb_elements: INTEGER) is -- crop local highscore to 'nb_elements' require positive: nb_elements > 0 do local_highscore.keep_first (nb_elements.min (local_highscore.count)) end sort_local_by_name (sort_reverse: BOOLEAN) is -- sort local highscore by name do sort (local_highscore, name, sort_reverse) end sort_local_by_score (sort_reverse: BOOLEAN) is -- sort local highscore by score do sort (local_highscore, score, sort_reverse) end sort_local_by_key (a_key: STRING; sort_reverse: BOOLEAN) is -- sort local highscore by a key (alphabetical sorting) do sort (local_highscore, a_key, sort_reverse) end feature {NONE} -- Sort Implementation sort (a_highscore: like local_highscore; a_key: STRING; is_reverse: BOOLEAN) is -- sort the highscore (using bubble sort) require a_highscore /= void key_valid (a_key) or a_key.is_equal (name) or a_key.is_equal(score) local changes_made: BOOLEAN i: INTEGER -- todo: temporary stuff only loop_count: INTEGER one, two: like highscore_entries do -- "name" and "score" are treated like special keys, which makes the feature -- somewhat bloated. This could be split up in several features. from changes_made := true loop_count := 0 until not changes_made loop from i := a_highscore.count changes_made := false until i <= 1 loop one := a_highscore.item(i) two := a_highscore.item (i - 1) if is_reverse then if a_key.is_equal (name) then if a_highscore.item (i).name < a_highscore.item (i - 1).name then a_highscore.swap (i, i - 1) changes_made := true end elseif a_key.is_equal (score) then if a_highscore.item (i).score < a_highscore.item (i - 1).score then a_highscore.swap (i, i - 1) changes_made := true end else if a_highscore.item (i).less_than(a_highscore.item(i - 1), a_key) then a_highscore.swap (i, i - 1) changes_made := true end end else if a_key.is_equal (name) then if a_highscore.item (i - 1).name < a_highscore.item (i).name then a_highscore.swap (i, i - 1) changes_made := true end elseif a_key.is_equal (score) then if a_highscore.item (i - 1).score < a_highscore.item (i).score then a_highscore.swap (i, i - 1) changes_made := true end else if a_highscore.item (i - 1).less_than(a_highscore.item(i), a_key) then a_highscore.swap (i, i - 1) changes_made := true end end end i := i - 1 end loop_count := loop_count + 1 end debug io.put_string ("Sorting required " + loop_count.out + " loops%N") end end feature -- Access highscore_file: PLAIN_TEXT_FILE highscore_dir: KL_DIRECTORY local_highscore: DS_LINKED_LIST[like highscore_entries] -- data structure with local highscore in it num_of_local_entries: INTEGER -- desired number of entries in local highscore feature -- Checksum checksum_generator: EM_CHECKSUM_GENERATOR set_checksum_generator (a_generator: like checksum_generator) is -- set the checksum generator -- (if you want to use your own checksum generator) do checksum_generator := a_generator -- tell the xml-filter to use this checksum generator highscore_checksum_filter.set_checksum_generator (a_generator) end feature {NONE} -- Attributes xml_section: STRING is "highscore" name: STRING is "name" score: STRING is "score" feature -- Keys other_keys: ARRAY[STRING] key_valid(the_key: STRING): BOOLEAN is -- check if a key is valid require key_not_void: the_key /= void do Result := other_keys.has (the_key) end has_keywords (the_keys: ARRAY[STRING]): BOOLEAN is -- check if an array has a keyword require the_keys /= void do the_keys.compare_objects Result := the_keys.has (name) or the_keys.has (score) end feature {NONE} -- Client ID client_id: INTEGER generate_client_id: INTEGER is -- generate a random client id local rnd: RANDOM do create rnd.make rnd.set_seed (time.ticks \\ client_id.max_value) Result := rnd.i_th (1) end feature {NONE} -- Attributes Implementation highscore_parser: XM_EIFFEL_PARSER highscore_xm_filter: EM_HIGHSCORE_XM_CALLBACKS_FILTER highscore_checksum_filter: EM_XM_CHECKSUM_FILTER highscore_entries: EM_HIGHSCORE_ENTRY -- dummy element end