Searching for dupes, then searching again, changes the data shown and is confusing (with caching on)

**Desktop**
 - Czkawka version: 6.0.0 gui
 - OS version: Mac OS 13.5.2
 - hardware: MacBook Pro 2021 (apple silicon)
 - compile and run method: as per instructions, having previously done `brew install rustup`

**Bug Description**

Ensure caching is on.

Create a folder "reference" with some test data (images or whatever), with no dupes in it. Copy the folder to another called "other".

Add the two folders into czkawka. Select "Reference" checkbox next to the "reference" folder.

Do a duplicate file search and then "Select -> Select all". The results look sensible: there's a ref folder listed above every dupe in a non-reference folder.

<image>

Now click search again, and the display changes: there is only one reference folder file listed, and a bunch of matches in the other folder. Do "Select -> Select all". It will select everything except the one reference folder file.

<image>

This is confusing; it now looks to me that this means hitting 'delete' will delete original data, because the 'other' folder matches are non-matching files amongst themselves, which are all now ticked. In fact, I don't think original data *is* going to be deleted; it's just that some dupes in the reference folder are now missing from the display.

Basically, hitting search should be idempotent: the result shouldn't change if you haven't changed anything since the last search.

When I turn caching off and delete existing cache files, I don't get this issue.

P.S. another issue, which I haven't go repro steps for but may be related to the issue above: I've search for dupes than deleted them, then searched again immediately with all same settings and more duplicates come up. I've seen this 2-3 times now.

diff --git a/Changelog.md b/Changelog.md
index b88306157..7ebc7de62 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,9 +1,17 @@
 ## Version 6.1.0 - ?
-- BREAKING CHANGE - Changed cache saving method, deduplicated, optimized and simplified procedure - [#1072](https://github.com/qarmin/czkawka/pull/1072)
+- BREAKING CHANGE - Changed cache saving method, deduplicated, optimized and simplified procedure(all files needs to be hashed again) - [#1072](https://github.com/qarmin/czkawka/pull/1072)
 - Remove up to 170ms of delay after ending scan - [#1070](https://github.com/qarmin/czkawka/pull/1070)
 - Added logger with useful info when debugging app (level can be adjusted via e.g. `RUST_LOG=debug` env) - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1070](https://github.com/qarmin/czkawka/pull/1070)
-- Core code cleanup - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1070](https://github.com/qarmin/czkawka/pull/1070)
+- Core code cleanup - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1070](https://github.com/qarmin/czkawka/pull/1070), [#1082](https://github.com/qarmin/czkawka/pull/1082)
 - Updated list of bad extensions and support for finding invalid jar files - [#1070](https://github.com/qarmin/czkawka/pull/1070)
+- More default excluded items on Windows(like pagefile) - [#1074](https://github.com/qarmin/czkawka/pull/1074)
+- Unified printing/saving method to files/terminal and fixed some differences/bugs - [#1082](https://github.com/qarmin/czkawka/pull/1082)
+- Uses fun_time library to print how much functions take time - [#1082](https://github.com/qarmin/czkawka/pull/1082)
+- Added exporting results into json file format - [#1083](https://github.com/qarmin/czkawka/pull/1083)
+- Added new test/regression suite for CI - [#1083](https://github.com/qarmin/czkawka/pull/1083)
+- Added ability to use relative paths - [#1083](https://github.com/qarmin/czkawka/pull/1083)
+- Fixed stability problem, that could remove invalid file in CLI - [#1083](https://github.com/qarmin/czkawka/pull/1083)
+- Fixed problem with invalid cache loading - [#0000]
 - Fix Windows gui crashes by using gtk 4.6 instead 4.8 or 4.10 - [#992](https://github.com/qarmin/czkawka/pull/992)
 - Fixed printing info about duplicated music files - [#1016](https://github.com/qarmin/czkawka/pull/1016)
 - Fixed printing info about duplicated video files - [#1017](https://github.com/qarmin/czkawka/pull/1017)
diff --git a/README.md b/README.md
index 8e247fe1d..71080b9d2 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
   - Temporary Files - Finds temporary files
   - Similar Images - Finds images which are not exactly the same (different resolution, watermarks)
   - Similar Videos - Looks for visually similar videos
-  - Same Music - Searches for music with the same artist, album etc.
+  - Same Music - Searches for similar music by tags or by reading content and comparing it
   - Invalid Symbolic Links - Shows symbolic links which point to non-existent files/directories
   - Broken Files - Finds files that are invalid or corrupted
   - Bad Extensions - Lists files whose content not match with their extension
diff --git a/czkawka_core/src/duplicate.rs b/czkawka_core/src/duplicate.rs
index cf14bd7a2..96a34f4fa 100644
--- a/czkawka_core/src/duplicate.rs
+++ b/czkawka_core/src/duplicate.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::collections::{BTreeMap, HashSet};
 use std::fmt::Debug;
 use std::fs::File;
@@ -424,11 +425,14 @@ impl DuplicateFinder {
             debug!("prehash_load_cache_at_start - started diff between loaded and prechecked files");
             for (size, mut vec_file_entry) in mem::take(&mut self.files_with_identical_size) {
                 if let Some(cached_vec_file_entry) = loaded_hash_map.get(&size) {
-                    // TODO maybe hashset is not needed when using < 4 elements
-                    let cached_path_entries = cached_vec_file_entry.iter().map(|e| &e.path).collect::<HashSet<_>>();
+                    // TODO maybe hashmap is not needed when using < 4 elements
+                    let mut cached_path_entries: HashMap<&Path, FileEntry> = HashMap::new();
+                    for file_entry in cached_vec_file_entry {
+                        cached_path_entries.insert(&file_entry.path, file_entry.clone());
+                    }
                     for file_entry in vec_file_entry {
-                        if cached_path_entries.contains(&file_entry.path) {
-                            records_already_cached.entry(size).or_default().push(file_entry);
+                        if let Some(cached_file_entry) = cached_path_entries.remove(file_entry.path.as_path()) {
+                            records_already_cached.entry(size).or_default().push(cached_file_entry);
                         } else {
                             non_cached_files_to_check.entry(size).or_default().push(file_entry);
                         }
@@ -508,7 +512,7 @@ impl DuplicateFinder {
         debug!("Starting calculating prehash");
         #[allow(clippy::type_complexity)]
         let pre_hash_results: Vec<(u64, BTreeMap<String, Vec<FileEntry>>, Vec<String>)> = non_cached_files_to_check
-            .par_iter()
+            .into_par_iter()
             .map(|(size, vec_file_entry)| {
                 let mut hashmap_with_hash: BTreeMap<String, Vec<FileEntry>> = Default::default();
                 let mut errors: Vec<String> = Vec::new();
@@ -519,15 +523,16 @@ impl DuplicateFinder {
                     check_was_stopped.store(true, Ordering::Relaxed);
                     return None;
                 }
-                for file_entry in vec_file_entry {
-                    match hash_calculation(&mut buffer, file_entry, &check_type, 0) {
+                for mut file_entry in vec_file_entry {
+                    match hash_calculation(&mut buffer, &file_entry, &check_type, 0) {
                         Ok(hash_string) => {
-                            hashmap_with_hash.entry(hash_string.clone()).or_default().push(file_entry.clone());
+                            file_entry.hash = hash_string.clone();
+                            hashmap_with_hash.entry(hash_string.clone()).or_default().push(file_entry);
                         }
                         Err(s) => errors.push(s),
                     }
                 }
-                Some((*size, hashmap_with_hash, errors))
+                Some((size, hashmap_with_hash, errors))
             })
             .while_some()
             .collect();
@@ -581,11 +586,14 @@ impl DuplicateFinder {
             debug!("full_hashing_load_cache_at_start - started diff between loaded and prechecked files");
             for (size, mut vec_file_entry) in pre_checked_map {
                 if let Some(cached_vec_file_entry) = loaded_hash_map.get(&size) {
-                    // TODO maybe hashset is not needed when using < 4 elements
-                    let cached_path_entries = cached_vec_file_entry.iter().map(|e| &e.path).collect::<HashSet<_>>();
+                    // TODO maybe hashmap is not needed when using < 4 elements
+                    let mut cached_path_entries: HashMap<&Path, FileEntry> = HashMap::new();
+                    for file_entry in cached_vec_file_entry {
+                        cached_path_entries.insert(&file_entry.path, file_entry.clone());
+                    }
                     for file_entry in vec_file_entry {
-                        if cached_path_entries.contains(&file_entry.path) {
-                            records_already_cached.entry(size).or_default().push(file_entry);
+                        if let Some(cached_file_entry) = cached_path_entries.remove(file_entry.path.as_path()) {
+                            records_already_cached.entry(size).or_default().push(cached_file_entry);
                         } else {
                             non_cached_files_to_check.entry(size).or_default().push(file_entry);
                         }
diff --git a/czkawka_core/src/similar_images.rs b/czkawka_core/src/similar_images.rs
index c08017243..c797522ca 100644
--- a/czkawka_core/src/similar_images.rs
+++ b/czkawka_core/src/similar_images.rs
@@ -783,7 +783,7 @@ impl SimilarImages {
         // Validating if group contains duplicated results
         let mut result_hashset: HashSet<String> = Default::default();
         let mut found = false;
-        // dbg!(collected_similar_images.len());
+
         for vec_file_entry in collected_similar_images.values() {
             if vec_file_entry.is_empty() {
                 println!("Empty group");
@@ -1338,7 +1338,6 @@ mod tests {

             similar_images.find_similar_hashes(None, None);
             let res = similar_images.get_similar_images();
-            // dbg!(&res);
             assert!(res.is_empty());
         }
     }


Users reconnecting to the game during post-legislative stages are shown incorrect policy card.

If a user loses connection during the post-legislative stage (or during a presidential power activation) and reconnects, they are shown a Liberal policy in the "Policy Enacted" dialogue, regardless of which policy was actually passed.
![image](https://user-images.githubusercontent.com/30200665/147520244-8ce7c28c-38b8-4736-ac39-32951902b875.png)

diff --git a/backend/src/main/java/game/SecretHitlerGame.java b/backend/src/main/java/game/SecretHitlerGame.java
index 775d6d1..81dcc3c 100644
--- a/backend/src/main/java/game/SecretHitlerGame.java
+++ b/backend/src/main/java/game/SecretHitlerGame.java
@@ -67,7 +67,7 @@ public class SecretHitlerGame implements Serializable {
     // The last president and chancellor that were successfully voted into office.
     private String lastPresident;
     private String lastChancellor;
-    private Policy.Type lastEnactedPolicy;
+    private Policy.Type lastEnactedPolicy = Policy.Type.FASCIST;

     private String currentPresident;
     private String currentChancellor;
diff --git a/backend/src/main/java/server/SecretHitlerServer.java b/backend/src/main/java/server/SecretHitlerServer.java
index 9be2e7d..20fa086 100644
--- a/backend/src/main/java/server/SecretHitlerServer.java
+++ b/backend/src/main/java/server/SecretHitlerServer.java
@@ -618,7 +618,7 @@ private static void onWebSocketMessage(WsMessageContext ctx) {
                         break;

                     case COMMAND_GET_STATE: // Requests the updated state of the game.
-                        lobby.updateUser(ctx);
+                        lobby.updateUser(ctx, name);
                         break;

                     case COMMAND_NOMINATE_CHANCELLOR: // params: PARAM_TARGET (String)
diff --git a/backend/src/main/java/server/util/GameToJSONConverter.java b/backend/src/main/java/server/util/GameToJSONConverter.java
index 23dbcb8..b3b1b75 100644
--- a/backend/src/main/java/server/util/GameToJSONConverter.java
+++ b/backend/src/main/java/server/util/GameToJSONConverter.java
@@ -2,6 +2,7 @@

 import game.GameState;
 import game.SecretHitlerGame;
+import game.datastructures.Identity;
 import game.datastructures.Player;
 import game.datastructures.Policy;
 import org.json.JSONObject;
@@ -14,34 +15,50 @@
 public class GameToJSONConverter {
     /**
      * Creates a JSON object from a SecretHitlerGame that represents its state.
+     *
      * @param game the SecretHitlerGame to convert.
+     * @param name the name of the user to create JSON content for. This is used
+     *             to determine what player identity information to send to the
+     *             user.
      * @throws NullPointerException if {@code game} is null.
      * @return a JSONObject with the following properties:
-     *          - {@code state}: the state of the game.
-     *          - {@code player-order}: an array of names representing the order of the players in the game.
+     *         - {@code state}: the state of the game.
+     *         - {@code player-order}: an array of names representing the order of
+     *         the players in the game.
      *
-     *          - {@code players}: a JSONObject map, with keys that are a player's {@code username}.
-     *              Each {@code username} key maps to an object with the properties {@code id} (String),
-     *              {@code alive} (boolean), and {@code investigated} (boolean), to represent the player.
-     *              The identity is either this.HITLER, this.FASCIST, or this.LIBERAL.
-     *              Ex: {"player1":{"alive": true, "investigated": false, "id": "LIBERAL"}}.
+     *         - {@code players}: a JSONObject map, with keys that are a player's
+     *         {@code username}.
+     *         Each {@code username} key maps to an object with the properties
+     *         {@code id} (String),
+     *         {@code alive} (boolean), and {@code investigated} (boolean), to
+     *         represent the player.
+     *         The identity is either this.HITLER, this.FASCIST, or this.LIBERAL.
+     *         Ex: {"player1":{"alive": true, "investigated": false, "id":
+     *         "LIBERAL"}}.
      *
-     *          - {@code president}: the username of the current president.
-     *          - {@code chancellor}: the username of the current chancellor (can be null).
-     *          - {@code last-president}: The username of the last president that presided over a legislative session.
-     *          - {@code last-chancellor}: The username of the last chancellor that presided over a legislative session.
-     *          - {@code draw-size}: The size of the draw deck.
-     *          - {@code discard-size}: The size of the discard deck.
-     *          - {@code fascist-policies}: The number of passed fascist policies.
-     *          - {@code liberal-policies}: The number of passed liberal policies.:
-     *          - {@code user-votes}: A map from each user to their vote from the last chancellor nomination.
-     *          - {@code president-choices}: The choices for the president during the legislative session (only if in
-     *                  game state LEGISLATIVE_PRESIDENT).
-     *          - {@code chancellor-choices}: The choices for the chancellor during the legislative session (only if in
-     *                  game state LEGISLATIVE_CHANCELLOR).
-     *          - {@code veto-occurred}: Set to true if a veto has already taken place on this legislative session.
+     *         - {@code president}: the username of the current president.
+     *         - {@code chancellor}: the username of the current chancellor (can be
+     *         null).
+     *         - {@code last-president}: The username of the last president that
+     *         presided over a legislative session.
+     *         - {@code last-chancellor}: The username of the last chancellor that
+     *         presided over a legislative session.
+     *         - {@code draw-size}: The size of the draw deck.
+     *         - {@code discard-size}: The size of the discard deck.
+     *         - {@code fascist-policies}: The number of passed fascist policies.
+     *         - {@code liberal-policies}: The number of passed liberal policies.:
+     *         - {@code user-votes}: A map from each user to their vote from the
+     *         last chancellor nomination.
+     *         - {@code president-choices}: The choices for the president during the
+     *         legislative session (only if in
+     *         game state LEGISLATIVE_PRESIDENT).
+     *         - {@code chancellor-choices}: The choices for the chancellor during
+     *         the legislative session (only if in
+     *         game state LEGISLATIVE_CHANCELLOR).
+     *         - {@code veto-occurred}: Set to true if a veto has already taken
+     *         place on this legislative session.
      */
-    public static JSONObject convert(SecretHitlerGame game) {
+    public static JSONObject convert(SecretHitlerGame game, String userName) {
         if (game == null) {
             throw new NullPointerException();
         }
@@ -51,14 +68,22 @@ public static JSONObject convert(SecretHitlerGame game) {
         String[] playerOrder = new String[game.getPlayerList().size()];
         List<Player> playerList = game.getPlayerList();

+        // Players should only be shown all roles under specific circumstances.
+        Identity role = game.getPlayer(userName).getIdentity();
+        boolean showAllRoles = game.hasGameFinished() || role == Identity.FASCIST
+                || (role == Identity.HITLER && game.getPlayerList().size() <= 6);
+
         for (int i = 0; i < playerList.size(); i++) {
             JSONObject playerObj = new JSONObject();
             Player player = playerList.get(i);

             playerObj.put("alive", player.isAlive());

-            String id = player.getIdentity().toString();
-            playerObj.put("id", id);
+            // Only include player role for self or under specific rules
+            if (player.getUsername().equals(userName) || showAllRoles) {
+                String id = player.getIdentity().toString();
+                playerObj.put("id", id);
+            }
             playerObj.put("investigated", player.hasBeenInvestigated());

             playerData.put(player.getUsername(), playerObj);
@@ -79,6 +104,8 @@ public static JSONObject convert(SecretHitlerGame game) {
         out.put("election-tracker", game.getElectionTracker());
         out.put("election-tracker-advanced", game.didElectionTrackerAdvance());

+        out.put("last-policy", game.getLastEnactedPolicy().toString().toUpperCase());
+
         out.put("draw-size", game.getDrawSize());
         out.put("discard-size", game.getDiscardSize());
         out.put("fascist-policies", game.getNumFascistPolicies());
@@ -101,13 +128,15 @@ public static JSONObject convert(SecretHitlerGame game) {

     /**
      * Converts a list of policies into a string array.
+     *
      * @param list the list of policies.
-     * @return a string array with the same length as the list, where each index is either "FASCIST" or "LIBERAL"
+     * @return a string array with the same length as the list, where each index is
+     *         either "FASCIST" or "LIBERAL"
      *         according to the type of the Policy at that index in the list.
      */
     public static String[] convertPolicyListToStringArray(List<Policy> list) {
         String[] out = new String[list.size()];
-        for(int i = 0; i < list.size(); i++) {
+        for (int i = 0; i < list.size(); i++) {
             out[i] = list.get(i).getType().toString();
         }
         return out;
diff --git a/backend/src/main/java/server/util/Lobby.java b/backend/src/main/java/server/util/Lobby.java
index afeaf26..57c2b43 100644
--- a/backend/src/main/java/server/util/Lobby.java
+++ b/backend/src/main/java/server/util/Lobby.java
@@ -10,6 +10,7 @@
 import java.io.IOException;
 import java.io.Serializable;
 import java.util.*;
+import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ConcurrentSkipListSet;
@@ -291,8 +292,8 @@ synchronized public int getUserCount() {
      *          updates all connected CpuPlayers after a set amount of time.
      */
     synchronized public void updateAllUsers() {
-        for (WsContext ws : userToUsername.keySet()) {
-            updateUser(ws);
+        for (Entry<WsContext, String> entry : userToUsername.entrySet()) {
+            updateUser(entry.getKey(), entry.getValue());
         }

         // Check if the game ended.
@@ -365,10 +366,10 @@ public void run() {
      *          SecretHitlerGame is sent
      *          to the specified WsContext. ({@code GameToJSONConverter.convert()})
      */
-    synchronized public void updateUser(WsContext ctx) {
+    synchronized public void updateUser(WsContext ctx, String userName) {
         JSONObject message;
         if (isInGame()) {
-            message = GameToJSONConverter.convert(game); // sends the game state
+            message = GameToJSONConverter.convert(game, userName); // sends the game state
             message.put(SecretHitlerServer.PARAM_PACKET_TYPE, SecretHitlerServer.PACKET_GAME_STATE);
         } else {
             message = new JSONObject();
diff --git a/frontend/src/App.js b/frontend/src/App.js
index d4f0a26..4298e86 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -53,6 +53,7 @@ import {
     PARAM_ELEC_TRACKER_ADVANCED,
     COMMAND_END_TERM,
     PARAM_LAST_STATE,
+    PARAM_LAST_POLICY,
     STATE_PP_PEEK,
     PLAYER_IS_ALIVE,
     PARAM_TARGET,
@@ -921,7 +922,7 @@ class App extends Component {
                 this.queueAlert((
                     <PolicyEnactedAlert
                         hideAlert={this.hideAlertAndFinish}
-                        policyType={liberalChanged ? LIBERAL : FASCIST}/>
+                        policyType={newState[PARAM_LAST_POLICY]}/>
                 ));
             }

diff --git a/frontend/src/GlobalDefinitions.js b/frontend/src/GlobalDefinitions.js
index ed8d5ef..8860720 100644
--- a/frontend/src/GlobalDefinitions.js
+++ b/frontend/src/GlobalDefinitions.js
@@ -118,5 +118,6 @@ export const PARAM_DISCARD_DECK = "discard-size";
 export const PARAM_PRESIDENT_CHOICES = "president-choices";
 export const PARAM_CHANCELLOR_CHOICES = "chancellor-choices";
 export const PARAM_TARGET = "target-user";
+export const PARAM_LAST_POLICY = "last-policy";
 export const PARAM_DID_VETO_OCCUR = "veto-occurred";
 // </editor-fold>
diff --git a/frontend/src/util/PolicyDisplay.js b/frontend/src/util/PolicyDisplay.js
index 3b532a3..97c910b 100644
--- a/frontend/src/util/PolicyDisplay.js
+++ b/frontend/src/util/PolicyDisplay.js
@@ -1,37 +1,48 @@
-import React, {Component} from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import './PolicyDisplay.css';
-import {LIBERAL} from "../GlobalDefinitions";
+import "./PolicyDisplay.css";
+import { LIBERAL } from "../GlobalDefinitions";
 import LiberalPolicy from "../assets/policy-liberal.png";
 import FascistPolicy from "../assets/policy-fascist.png";

 class PolicyDisplay extends Component {
-    render() {
-        return (
-

-                {this.props.policies.map((value, index) => {
-                    let policyName = value === LIBERAL ? "liberal" : "fascist";
-                    return (
-                         this.props.onClick(index)}
-                            disabled={!this.props.allowSelection}
-                            src={value === LIBERAL ? LiberalPolicy : FascistPolicy} // Toggles fascist/liberal policy
-                            alt={"A " + policyName + " policy." + (this.props.allowSelection ? " Click to select." : "")}
-                        />
-                    );
-                } )}
-

-        );
-    }
+	render() {
+		return (
+

+				{this.props.policies.map((value, index) => {
+					let policyName = value === LIBERAL ? "liberal" : "fascist";
+					return (
+						 this.props.onClick(index)}
+							disabled={!this.props.allowSelection}
+							src={value === LIBERAL ? LiberalPolicy : FascistPolicy} // Toggles fascist/liberal policy
+							alt={
+								"A " +
+								policyName +
+								" policy." +
+								(this.props.allowSelection ? " Click to select." : "")
+							}
+						/>
+					);
+				})}
+

+		);
+	}
 }

 PolicyDisplay.propTypes = {
-    policies: PropTypes.array.isRequired,
-    onClick: PropTypes.func,  // If undefined, the policies cannot be selected.
-    selection: PropTypes.number,
-    allowSelection: PropTypes.bool,
+	policies: PropTypes.array.isRequired,
+	onClick: PropTypes.func, // If undefined, the policies cannot be selected.
+	selection: PropTypes.number,
+	allowSelection: PropTypes.bool,
 };

-export default PolicyDisplay;
\ No newline at end of file
+export default PolicyDisplay;