Repository: bitburner-official/bitburner-src
Pull request #2113: MISC: Updated createRandomIP to use the full 32 bit space
Author: login: whiskeyfur
Created: 2025-04-27T18:05:09Z | Merged: 2025-05-14T08:17:52Z

MISC: Updated createRandomIP to return full Ip addresses using the entire range, instead of xx.x.x.x.

Tested by 
1: starting a new game ("delete save")
2: used "ipaddr" at the terminal line.

Results:
![image](https://github.com/user-attachments/assets/5ec67354-5547-4ac2-aa76-e582e2db8b06)

Impact:
NO foreseeable impact. All this does is change the string, does not change type, and does not change the formatting. Only expands on the range of numbers to be found for each octet.



Diff:
diff --git a/src/utils/IPAddress.ts b/src/utils/IPAddress.ts
index 0c1ba1c2c1..740e2452b3 100644
--- a/src/utils/IPAddress.ts
+++ b/src/utils/IPAddress.ts
@@ -1,10 +1,21 @@
 import type { IPAddress } from "../Types/strings";
-import { getRandomByte } from "./helpers/getRandomByte";
 
 /**
  * Generate a random IP address
  * Does not check to see if the IP already exists in the game
  */
 export const createRandomIp = (): IPAddress => {
-  return `${getRandomByte(99)}.${getRandomByte(9)}.${getRandomByte(9)}.${getRandomByte(9)}` as IPAddress;
+  // Credit goes to yichizhng on BitBurner discord
+  // Generates a number like 0.c8f0a07f1d47e8
+  const ip = Math.random().toString(16);
+  // uses regex to match every 2 characters. [0.][c8][f0][a0][7f][1d][47][e8]
+  // we only want #1 through #4
+  const matchResult = ip.match(/../g);
+  if (!matchResult) {
+    // This case should never happen.
+    throw new Error(`Unexpected regex matching bug in createRandomIp. ip: ${ip}`);
+  }
+  const sliced = matchResult.slice(1, 5);
+  //convert each to a decimal number and join them together to make a human readable IP address.
+  return sliced.map((x) => parseInt(x, 16)).join(".") as IPAddress;
 };
diff --git a/test/jest/__snapshots__/FullSave.test.ts.snap b/test/jest/__snapshots__/FullSave.test.ts.snap
index 823ef3ef51..2215537efb 100644
--- a/test/jest/__snapshots__/FullSave.test.ts.snap
+++ b/test/jest/__snapshots__/FullSave.test.ts.snap
@@ -88,60 +88,60 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "City",
             "data": {
               "chaos": 0,
-              "comms": 42,
+              "comms": 30,
               "name": "Aevum",
-              "pop": 1101351351,
-              "popEst": 803688823.7027026,
+              "pop": 1060810810,
+              "popEst": 688093498.3783784,
             },
           },
           "Chongqing": {
             "ctor": "City",
             "data": {
               "chaos": 0,
-              "comms": 54,
+              "comms": 42,
               "name": "Chongqing",
-              "pop": 1141891892,
-              "popEst": 925858290.8108107,
+              "pop": 1101351351,
+              "popEst": 803688823.7027026,
             },
           },
           "Ishima": {
             "ctor": "City",
             "data": {
               "chaos": 0,
-              "comms": 89,
+              "comms": 78,
               "name": "Ishima",
-              "pop": 1263513514,
-              "popEst": 1331811541.7837837,
+              "pop": 1222972973,
+              "popEst": 1189919649.4054055,
             },
           },
           "New Tokyo": {
             "ctor": "City",
             "data": {
               "chaos": 0,
-              "comms": 78,
+              "comms": 66,
               "name": "New Tokyo",
-              "pop": 1222972973,
-              "popEst": 1189919649.4054055,
+              "pop": 1182432432,
+              "popEst": 1054601898.8108108,
             },
           },
           "Sector-12": {
             "ctor": "City",
             "data": {
               "chaos": 0,
-              "comms": 66,
+              "comms": 54,
               "name": "Sector-12",
-              "pop": 1182432432,
-              "popEst": 1054601898.8108108,
+              "pop": 1141891892,
+              "popEst": 925858290.8108107,
             },
           },
           "Volhaven": {
             "ctor": "City",
             "data": {
               "chaos": 0,
-              "comms": 101,
+              "comms": 89,
               "name": "Volhaven",
-              "pop": 1304054054,
-              "popEst": 1480277574.8108108,
+              "pop": 1263513514,
+              "popEst": 1331811541.7837837,
             },
           },
         },
@@ -156,7 +156,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Contract",
             "data": {
               "autoLevel": true,
-              "count": 117,
+              "count": 105,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -167,7 +167,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Contract",
             "data": {
               "autoLevel": true,
-              "count": 125,
+              "count": 113,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -178,7 +178,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Contract",
             "data": {
               "autoLevel": true,
-              "count": 115,
+              "count": 105,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -203,7 +203,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Operation",
             "data": {
               "autoLevel": true,
-              "count": 23,
+              "count": 11,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -215,7 +215,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Operation",
             "data": {
               "autoLevel": true,
-              "count": 88,
+              "count": 80,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -227,7 +227,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Operation",
             "data": {
               "autoLevel": true,
-              "count": 7,
+              "count": 144,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -239,7 +239,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Operation",
             "data": {
               "autoLevel": true,
-              "count": 15,
+              "count": 3,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -251,7 +251,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Operation",
             "data": {
               "autoLevel": true,
-              "count": 148,
+              "count": 136,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -263,7 +263,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             "ctor": "Operation",
             "data": {
               "autoLevel": true,
-              "count": 94,
+              "count": 86,
               "failures": 0,
               "level": 1,
               "maxLevel": 1,
@@ -272,7 +272,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
             },
           },
         },
-        "randomEventCounter": 303,
+        "randomEventCounter": 274,
         "rank": 2000,
         "skillPoints": 666,
         "skills": {},


Reviews:
user: login: d0sboots
body: I was going to take this as-is, but since you've got a failing test to patch up, I'll suggest some ways to reduce the verbosity a bit.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T09:21:53Z
user: login: catloversg
body: You should also run `npm run test` and check its result. This change makes the FullSave test fail. The reason is fairly complicated, especially if you are not familiar with our codebase. To fix this problem, the easiest way is to run `npm run test -- -u`. It will update the Jest snapshot file.

If you are unsure what this means, you can overwrite `test/jest/__snapshots__/FullSave.test.ts.snap` with my attached file and commit it.

[FullSave.test.ts.snap.txt](https://github.com/user-attachments/files/20155801/FullSave.test.ts.snap.txt)

Remember to rename it back to `FullSave.test.ts.snap`.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T06:52:45Z
user: login: catloversg
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T06:26:27Z

Review comments:
diff_hunk: @@ -1,10 +1,19 @@
 import type { IPAddress } from "../Types/strings";
-import { getRandomByte } from "./helpers/getRandomByte";
 
 /**
  * Generate a random IP address
  * Does not check to see if the IP already exists in the game
  */
 export const createRandomIp = (): IPAddress => {
-  return `${getRandomByte(99)}.${getRandomByte(9)}.${getRandomByte(9)}.${getRandomByte(9)}` as IPAddress;
+  // Credit goes to yichizhng on BitBurner discord
+  // Generates a number like 0.c8f0a07f1d47e8
+  const ip = Math.random().toString(16);
+  // uses regex to match every 2 characters. [0.][c8][f0][a0][7f][1d][47][e8]
+  const matchArray = ip.match(/../g) as RegExpMatchArray;
+  // we only want #1 through #4
+  const sliced = matchArray.slice(1, 5);
path: src/utils/IPAddress.ts
user: login: d0sboots
body: ```suggestion
  // uses regex to match every 2 characters. [0.][c8][f0][a0][7f][1d][47][e8]
  // we only want #1 through #4
  const sliced = ip.match(/../g).slice(1, 5);
```
created_at: 2025-05-10T09:19:48Z
author_association: COLLABORATOR
diff_hunk: @@ -1,10 +1,19 @@
 import type { IPAddress } from "../Types/strings";
-import { getRandomByte } from "./helpers/getRandomByte";
 
 /**
  * Generate a random IP address
  * Does not check to see if the IP already exists in the game
  */
 export const createRandomIp = (): IPAddress => {
-  return `${getRandomByte(99)}.${getRandomByte(9)}.${getRandomByte(9)}.${getRandomByte(9)}` as IPAddress;
+  // Credit goes to yichizhng on BitBurner discord
+  // Generates a number like 0.c8f0a07f1d47e8
+  const ip = Math.random().toString(16);
+  // uses regex to match every 2 characters. [0.][c8][f0][a0][7f][1d][47][e8]
+  const matchArray = ip.match(/../g) as RegExpMatchArray;
+  // we only want #1 through #4
+  const sliced = matchArray.slice(1, 5);
+  //convert each to a decimal number
+  const asDecimals = sliced.map((x) => parseInt(x, 16));
+  // and join them together to make a human readable IP address.
+  return asDecimals.join(".") as IPAddress;
path: src/utils/IPAddress.ts
user: login: d0sboots
body: ```suggestion
  //convert each to a decimal number and join them together to make a human readable IP address.
  return sliced.map((x) => parseInt(x, 16)).join(".") as IPAddress;
```
created_at: 2025-05-10T09:19:55Z
author_association: COLLABORATOR
diff_hunk: @@ -1,10 +1,16 @@
 import type { IPAddress } from "../Types/strings";
-import { getRandomByte } from "./helpers/getRandomByte";
 
 /**
  * Generate a random IP address
  * Does not check to see if the IP already exists in the game
  */
 export const createRandomIp = (): IPAddress => {
-  return `${getRandomByte(99)}.${getRandomByte(9)}.${getRandomByte(9)}.${getRandomByte(9)}` as IPAddress;
+  // Credit goes to yichizhng on BitBurner discord
+  // Generates a number like 0.c8f0a07f1d47e8
+  const ip = Math.random().toString(16);
+  // uses regex to match every 2 characters. [0.][c8][f0][a0][7f][1d][47][e8]
+  // we only want #1 through #4
+  const sliced = ip.match(/../g).slice(1, 5);
path: src/utils/IPAddress.ts
user: login: catloversg
body: ```suggestion
  const matchResult = ip.match(/../g);
  if (!matchResult) {
    // This case should never happen.
    throw new Error(`Unexpected regex matching bug in createRandomIp. ip: ${ip}`);
  }
  const sliced = matchResult.slice(1, 5);
```
`String.match` may return null. In this case, it's not a problem because `Math.random().toString(16)` always matches `/../g`, unless `Math.random` is overridden. Nonetheless, we should always check the null case.

I'm not sure why our lint setup (`npm run lint`) does not catch this case, but the lint setup in my IDE catches it.
created_at: 2025-05-12T06:44:36Z
author_association: COLLABORATOR

PR comments:
user: login: whiskeyfur
created_at: 2025-05-11T21:34:39Z
body: I'm perfectly fine with the suggested changes, accepted and comitted.
author_association: CONTRIBUTOR
user: login: whiskeyfur
created_at: 2025-05-12T22:52:21Z
body: Test Suite updated.
author_association: CONTRIBUTOR
user: login: d0sboots
created_at: 2025-05-14T08:16:33Z
body: Oh wow, I had to stare at the test changes for a full minute to understand what had happened. That *is* subtle.
author_association: COLLABORATOR
user: login: whiskeyfur
created_at: 2025-05-14T17:48:10Z
body: > Oh wow, I had to stare at the test changes for a full minute to understand what had happened. That _is_ subtle.

Looks like I'll have to contact you on discord to ask for an explanation because to me, it makes no sense at all why this change caused that test to fail. Population counts have nothing to do with ip addresses... or so I thought?
author_association: CONTRIBUTOR
user: login: d0sboots
created_at: 2025-05-14T19:25:34Z
body: The reason is because this changes how many random numbers are used by createRandomIP - it used to be 4, now it's 1. As a result, everything that uses random numbers after the point of server initialization (such as bladeburner actions) will end up with different random results.

Our tests use a deterministic seed, so they produce the *same* "random" results each time. But, when something upstream of them changes like this, it effects all the random rolls.
author_association: COLLABORATOR
user: login: whiskeyfur
created_at: 2025-05-15T01:30:26Z
body: Ah, the good ol' pseudo random problem when it comes to security in cryptography.

Makes perfect sense, thank you.
author_association: CONTRIBUTOR