Skip to content

Coming from another phone

A city that already has a phone has years of things in it: numbers people learned by heart, conversations, photos, @handles. Losing all of it is the real price of changing phone, and it is the reason most servers never do. This reads the old phone’s tables and writes ours.

Four phones can be come from, and all four are tested the same way: their own tables are created from the schema their installer ships, a small city is written into them, the import is run for real, and every row it claims is checked afterwards.

FromIts tablesWhat it keys everything by
lb-phonephone_*the phone number, with phone_phones turning one into an owner
Quasar Smartphone Proqs_phone_*a scope_id — one phone, not one person
NPWDnpwd_*the framework identifier, and its numbers are already in users
qb-phoneplayer_contacts, phone_messages…citizenid, which is this phone’s owner on QBCore

It never touches the old phone. Not one statement writes to a table that is not ours. lb-phone’s data is exactly as it was afterwards, which is what makes the way back simple: delete our rows.

The rehearsal is real. DryRun is on by default. It reads everything a real run reads, works out every row it would write, writes none of them, and prints the count. It is the same code — a rehearsal that took a shortcut would be a rehearsal of nothing.

It cannot happen twice. A real run leaves a receipt in the database. The next boot finds it and stops, because one forgotten config line would otherwise duplicate every conversation in the city.

-- shared/config/import.lua
Config.Import = {
From = 'lb-phone', -- or false, which is where it starts
DryRun = true, -- read the count first
Numbers = 'keep', -- their old numbers move across
Crypto = 'cash', -- sell their coins at their own price
}

Restart once and read the console:

[mic_phone import] lb-phone — REHEARSAL, nothing was written
[mic_phone import] contacts 482
[mic_phone import] messages 19104
[mic_phone import] numbers 137
[mic_phone import] photos 2210
[mic_phone import] social 96
[mic_phone import] · a phone whose owner is not in `users` any more was skipped (×8)
[mic_phone import] · lb-phone does not say whether an alarm repeats, so its alarms arrive as one-off alarms

Then DryRun = false, restart once more, and set From = false afterwards.

If the tables are not in this database it says so and does nothing, so naming the wrong phone costs you a line in the console.

Everything below is lb-phone, which is the fullest of the four. What each of the others does differently is further down.

From lb-phoneBecomes
phone_phonesthe number in users, the battery, the phone’s settings
its settings.appsthe Home Screen — the same dock, the same apps on the same pages
phone_phone_contactscontacts, with favourites and pictures
phone_message_*conversations, groups, and the unread counts
phone_phone_callsrecents, one row per side, missed calls included
phone_photosthe library, photos and videos
phone_mail_*mail, read and unread
phone_maps_locationssaved places
phone_marketplace_posts, phone_yellow_pages_postslistings
Instagram, Twitter, TikTok, TinderInstapic, Twixel, Klipz, Flinder — handles, names, pictures, bios, posts, follows, and the verified tick
phone_darkchat_*the darknet: the room, who was in it, what was said
phone_notes, phone_clock_alarmsnotes and alarms — lb-phone’s alarms do not say whether they repeat, so they arrive as one-off alarms
phone_cryptosee Crypto below

Pictures are URLs on both phones, so nothing is downloaded or re-uploaded. A player’s photos keep working on the first day because they were never either phone’s to begin with.

The report says each of these out loud rather than leaving somebody to notice.

  • Wallpapers and ringtones. Theirs name their own files, ours name ours. A wrong picture is worse than the default one.
  • Social passwords. See below — the accounts come, the passwords cannot.
  • Airplane mode, deliberately. Importing a switch somebody left on is importing a phone that does not work and a player who cannot tell why.
  • Photo albums. Every picture is in the library; the albums are not rebuilt.

The other three, and what is different about each

Section titled “The other three, and what is different about each”

The best built of the four, and the mapping is exact rather than approximate in three places:

  • Unread counts are recounted, not copied. Quasar stores last_read_message_id; ours stores a number. So the number is worked out — how many messages in that conversation came after the one they last read — which is right even if their own counter had drifted.
  • Recents already say incoming, outgoing and missed, which are our three words.
  • A photo knows it is a video, with its length and its poster, so it arrives as one.

Notes (pinned ones stay pinned), alarms, calendar entries and reminders come too: an open reminder with a due date becomes a calendar entry, one without a date keeps its words as a note, and a finished one is left behind. Quasar can repeat an alarm at weekends only, which this phone cannot, so those arrive as one-off alarms.

Pinned and muted conversations keep both. Mail comes with the folder it was in — drafts and trash are left behind, because a draft nobody sent is not worth moving. What does not come: their settings and home screen, which live in a JSON blob of Quasar’s own shape, and their App Store ids, which are not ours.

One thing to know: Quasar lets a character hold more than one phone, and this one keeps a number per character. The first number to land is theirs; any other is named in the report and nothing is overwritten.

The easiest, for a reason worth saying out loud: NPWD keeps the number in users.phone_number already, which is the same column this phone reads. On a default NPWD server there is nothing to move, and the report says so rather than pretending to work.

Its conversations are named by a string of the numbers in them rather than by an id, so the join is on that string. Its Twitter profile name becomes the @handle; Match has no username at all, so a Flinder account is keyed by the character’s number. Photos have no date of their own, so the library is dated by the order they were taken. Its notes come across the same way, dated by the order they were written. Listings carry no price and arrive at nothing.

The oldest, and the one that is least like us — but the identity is free, because on QBCore this phone already uses the citizenid that qb-phone keys everything by.

Its conversations are the work. qb-phone has no conversations: phone_messages is one row per person per number with the whole exchange as a JSON array, written from each side separately. So the same conversation exists twice and the two copies need not agree. Each pair of numbers is therefore built once, from whichever side has more in it — merging them would invent an order, and importing both would give everybody every message twice. A conversation with no timestamps keeps its order and is laid out a minute apart.

Numbers live in players.charinfo as JSON rather than in a column, so they are read out of there and written where this phone keeps them. Its tweets carry a name rather than an account, so one Twixel account is made per character from their name.

Numbers = 'keep' is the point of the whole exercise: the number on somebody’s business card, and in everybody else’s contacts, still reaches them.

A number is only ever written into an empty column. If a character already has one here and it is a different number, that is a clash: it is counted, named in the report, and nothing is overwritten. Their old numbers also keep their old shape — 1234567890 rather than this phone’s 555-###-#### — which is cosmetic and only affects the numbers handed out from then on.

Numbers = 'ours' leaves them alone and lets this phone hand out its own. Their old contacts will point at nobody, so it is only for a city starting again.

Their passwords are plain text in a column. Ours are a PBKDF2 hash made in the player’s browser, and one cannot be worked out from the other — so the password is the one thing that does not cross, and it is not stored anywhere on the way.

The account arrives marked as imported. The first time its owner opens the app, the phone says This account came over from your old phone. Choose a password for it, and that is the new one.

This is safe for a better reason than a password would be: the row names an owner, and the server knows who is asking without being told. Nothing is being proved by a password that was never ours.

There is no honest exchange rate between two invented markets, so pick what happens rather than having it guessed:

  • 'cash' — sell at the old phone’s own price and write it into the wallet as a movement. Nobody gains, nobody loses, and nothing has to be agreed.
  • 'skip' — leave it. Their holdings stay in their tables and can be dealt with by hand.
  • a table — your own mapping, when the holdings should survive as coins:
Crypto = { qbit = 'santo', lifeinvader = 'vine' },

Nothing of the old phone’s was touched, so:

  1. Delete our rows for whatever step was wrong — the report names them.
  2. Delete the receipt: DELETE FROM mic_phone_json WHERE owner = '@import'.
  3. Config.Import.Again = true, and go again.

Skip leaves individual steps out by the names the report prints:

Skip = { 'photos', 'crypto' },