mirror of https://github.com/tilde-team/botany
303 lines
12 KiB
Python
303 lines
12 KiB
Python
import errno
|
|
import getpass
|
|
import json
|
|
import os
|
|
import pickle
|
|
import sqlite3
|
|
import threading
|
|
import time
|
|
from glob import glob
|
|
|
|
import consts
|
|
|
|
|
|
class DataManager(object):
|
|
# handles user data, puts a .botany dir in user's home dir (OSX/Linux)
|
|
# handles shared data with sqlite db
|
|
# TODO: .dat save should only happen on mutation, water, death, exit,
|
|
# harvest, otherwise
|
|
# data hasn't changed...
|
|
# can write json whenever bc this isn't ever read for data within botany
|
|
|
|
user_dir = os.path.expanduser("~")
|
|
botany_dir = os.path.join(user_dir, '.botany')
|
|
game_dir = os.path.dirname(os.path.realpath(__file__))
|
|
this_user = getpass.getuser()
|
|
|
|
savefile_name = this_user + '_plant.dat'
|
|
savefile_path = os.path.join(botany_dir, savefile_name)
|
|
# set this.savefile_path to guest_garden path
|
|
|
|
garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
|
|
garden_json_path = os.path.join(game_dir, 'garden_file.json')
|
|
harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat')
|
|
harvest_json_path = os.path.join(botany_dir, 'harvest_file.json')
|
|
|
|
def __init__(self):
|
|
self.conn = sqlite3.connect(self.garden_db_path)
|
|
self.conn.row_factory = sqlite3.Row
|
|
self.cursor = self.conn.cursor()
|
|
self.last_water_gain = 0.0
|
|
self.this_user = getpass.getuser()
|
|
# check if instance is already running
|
|
# check for .botany dir in home
|
|
try:
|
|
os.makedirs(self.botany_dir)
|
|
except OSError as exception:
|
|
if exception.errno != errno.EEXIST:
|
|
raise
|
|
self.savefile_name = self.this_user + '_plant.dat'
|
|
|
|
def check_plant(self):
|
|
# check for existing save file
|
|
return os.path.isfile(self.savefile_path)
|
|
|
|
def start_threads(self, this_plant):
|
|
# create threads to save files every minute
|
|
death_check_thread = threading.Thread(target=self.death_check_update, args=(this_plant,))
|
|
death_check_thread.daemon = True
|
|
death_check_thread.start()
|
|
|
|
def death_check_update(self, this_plant):
|
|
# .1 second updates and lock to minimize race condition
|
|
while True:
|
|
is_dead = this_plant.dead_check()
|
|
if is_dead:
|
|
self.save_plant(this_plant)
|
|
self.data_write_json(this_plant)
|
|
self.update_garden_db(this_plant)
|
|
self.harvest_plant(this_plant)
|
|
this_plant.unlock_new_creation()
|
|
time.sleep(.1)
|
|
|
|
def load_plant(self):
|
|
# load savefile
|
|
with open(self.savefile_path, 'rb') as f:
|
|
this_plant = pickle.load(f)
|
|
|
|
# migrate data structure to create data for empty/nonexistent plant
|
|
# properties
|
|
this_plant.migrate_properties()
|
|
|
|
# get status since last login
|
|
is_watered = this_plant.water_check(self)
|
|
is_dead = this_plant.dead_check()
|
|
|
|
if not is_dead:
|
|
if is_watered:
|
|
time_delta_last = int(time.time()) - this_plant.last_time
|
|
ticks_to_add = min(time_delta_last, 24 * 3600)
|
|
this_plant.time_delta_watered = 0
|
|
self.last_water_gain = time.time()
|
|
else:
|
|
ticks_to_add = 0
|
|
this_plant.ticks += ticks_to_add * (0.2 * (this_plant.generation - 1) + 1)
|
|
return this_plant
|
|
|
|
@staticmethod
|
|
def plant_age_convert(this_plant):
|
|
# human-readable plant age
|
|
age_seconds = int(time.time()) - this_plant.start_time
|
|
days, age_seconds = divmod(age_seconds, 24 * 60 * 60)
|
|
hours, age_seconds = divmod(age_seconds, 60 * 60)
|
|
minutes, age_seconds = divmod(age_seconds, 60)
|
|
age_formatted = ("%dd:%dh:%dm:%ds" % (days, hours, minutes, age_seconds))
|
|
return age_formatted
|
|
|
|
def init_database(self):
|
|
# check if dir exists, create sqlite directory and set OS permissions to 777
|
|
sqlite_dir_path = os.path.join(self.game_dir, 'sqlite')
|
|
if not os.path.exists(sqlite_dir_path):
|
|
os.makedirs(sqlite_dir_path, mode=0o777)
|
|
|
|
for schema in glob(os.path.join(sqlite_dir_path, 'main', '*.sql')):
|
|
with open(schema) as schema_file:
|
|
self.cursor.execute(schema_file.read())
|
|
|
|
self.conn.commit()
|
|
|
|
# init only, creates and sets permissions for garden db and json
|
|
if os.name == "posix":
|
|
if os.stat(self.garden_db_path).st_uid == os.getuid():
|
|
os.chmod(self.garden_db_path, 0o666)
|
|
open(self.garden_json_path, 'a').close()
|
|
os.chmod(self.garden_json_path, 0o666)
|
|
|
|
def update_garden_db(self, this_plant):
|
|
# insert or update this plant id's entry in DB
|
|
# TODO: make sure other instances of user are deleted
|
|
# Could create a clean db function
|
|
age_formatted = self.plant_age_convert(this_plant)
|
|
# try to insert or replace
|
|
self.cursor.execute(
|
|
"""INSERT OR REPLACE INTO garden (
|
|
plant_id, owner, description, age, score, is_dead
|
|
) VALUES (
|
|
:pid, :owner, :description, :page, :score, :dead
|
|
)""",
|
|
{"pid": this_plant.plant_id,
|
|
"owner": this_plant.owner,
|
|
"description": this_plant.parse_plant(),
|
|
"page": age_formatted,
|
|
"score": str(this_plant.ticks),
|
|
"dead": int(this_plant.dead)})
|
|
self.conn.commit()
|
|
|
|
def retrieve_garden_from_db(self):
|
|
# Builds a dict of dicts from garden sqlite db
|
|
garden_dict = {}
|
|
# Need to allow write permissions by others
|
|
|
|
self.cursor.execute('SELECT * FROM garden ORDER BY owner')
|
|
tuple_list = self.cursor.fetchall()
|
|
# Building dict from table rows
|
|
for item in tuple_list:
|
|
garden_dict[item[0]] = {
|
|
"owner": item[1],
|
|
"description": item[2],
|
|
"age": item[3],
|
|
"score": item[4],
|
|
"dead": item[5],
|
|
}
|
|
return garden_dict
|
|
|
|
def update_garden_json(self):
|
|
with open(self.garden_json_path, 'w') as outfile:
|
|
json.dump(self.retrieve_garden_from_db(), outfile)
|
|
|
|
def save_plant(self, this_plant):
|
|
# create savefile
|
|
this_plant.last_time = int(time.time())
|
|
temp_path = self.savefile_path + ".temp"
|
|
with open(temp_path, 'wb') as f:
|
|
pickle.dump(this_plant, f, protocol=2)
|
|
os.replace(temp_path, self.savefile_path)
|
|
|
|
def data_write_json(self, this_plant):
|
|
# create personal json file for user to use outside the game (website?)
|
|
json_file = os.path.join(self.botany_dir, self.this_user + '_plant_data.json')
|
|
# also updates age
|
|
age_formatted = self.plant_age_convert(this_plant)
|
|
plant_info = {
|
|
"owner": this_plant.owner,
|
|
"description": this_plant.parse_plant(),
|
|
"age": age_formatted,
|
|
"score": this_plant.ticks,
|
|
"is_dead": this_plant.dead,
|
|
"last_watered": this_plant.watered_timestamp,
|
|
"file_name": this_plant.file_name,
|
|
"stage": consts.stages[this_plant.stage],
|
|
"generation": this_plant.generation,
|
|
}
|
|
if this_plant.stage >= 3:
|
|
plant_info["rarity"] = consts.rarities[this_plant.rarity]
|
|
if this_plant.mutation != 0:
|
|
plant_info["mutation"] = consts.mutations[this_plant.mutation]
|
|
if this_plant.stage >= 4:
|
|
plant_info["color"] = consts.colors[this_plant.color]
|
|
if this_plant.stage >= 2:
|
|
plant_info["species"] = consts.species[this_plant.species]
|
|
|
|
with open(json_file, 'w') as outfile:
|
|
json.dump(plant_info, outfile)
|
|
|
|
def harvest_plant(self, this_plant):
|
|
# TODO: plant history feature - could just use a sqlite query to retrieve all of user's dead plants
|
|
|
|
# harvest is a dict of dicts
|
|
# harvest contains one entry for each plant id
|
|
age_formatted = self.plant_age_convert(this_plant)
|
|
this_plant_id = this_plant.plant_id
|
|
plant_info = {
|
|
"description": this_plant.parse_plant(),
|
|
"age": age_formatted,
|
|
"score": this_plant.ticks,
|
|
}
|
|
if os.path.isfile(self.harvest_file_path):
|
|
# harvest file exists: load data
|
|
with open(self.harvest_file_path, 'rb') as f:
|
|
this_harvest = pickle.load(f)
|
|
new_file_check = False
|
|
else:
|
|
this_harvest = {}
|
|
new_file_check = True
|
|
|
|
this_harvest[this_plant_id] = plant_info
|
|
|
|
# dump harvest file
|
|
temp_path = self.harvest_file_path + ".temp"
|
|
with open(temp_path, 'wb') as f:
|
|
pickle.dump(this_harvest, f, protocol=2)
|
|
os.rename(temp_path, self.harvest_file_path)
|
|
# dump json file
|
|
with open(self.harvest_json_path, 'w') as outfile:
|
|
json.dump(this_harvest, outfile)
|
|
|
|
return new_file_check
|
|
|
|
def get_weekly_visitors(self, plant, maxx):
|
|
self.cursor.execute("SELECT * FROM visitors WHERE garden_name = ? ORDER BY weekly_visits", plant.owner)
|
|
visitor_data = self.cursor.fetchall()
|
|
visitor_block = ""
|
|
visitor_line = ""
|
|
if visitor_data:
|
|
for visitor in visitor_data:
|
|
visitor_name = visitor[2]
|
|
weekly_visits = visitor[3]
|
|
this_visitor_string = "{}({}) ".format(visitor_name, weekly_visits)
|
|
if len(visitor_line + this_visitor_string) > maxx - 3:
|
|
visitor_block += '\n'
|
|
visitor_line = ""
|
|
visitor_block += this_visitor_string
|
|
visitor_line += this_visitor_string
|
|
else:
|
|
visitor_block = 'nobody :('
|
|
return visitor_block
|
|
|
|
def update_visitor_db(self, visitor_names, owner_name):
|
|
for name in visitor_names:
|
|
self.cursor.execute(
|
|
"SELECT * FROM visitors WHERE garden_name = ? AND visitor_name = ?", (owner_name, name))
|
|
data = self.cursor.fetchone()
|
|
if data is None:
|
|
self.cursor.execute(" INSERT INTO visitors (garden_name,visitor_name,weekly_visits) VALUES(?, ?, 1)",
|
|
(owner_name, name))
|
|
else:
|
|
self.cursor.execute(
|
|
"UPDATE visitors SET weekly_visits = weekly_visits + 1 WHERE garden_name = ? AND visitor_name = ?",
|
|
(owner_name, name))
|
|
self.conn.commit()
|
|
|
|
def clear_weekly_visitors(self):
|
|
self.cursor.execute("DELETE FROM visitors")
|
|
self.conn.commit()
|
|
|
|
def load_visitors(self, plant):
|
|
visitor_filepath = os.path.join(os.path.join(os.path.expanduser("~"), '.botany'), 'visitors.json')
|
|
guest_timestamps = []
|
|
visitors_this_check = []
|
|
if os.path.isfile(visitor_filepath):
|
|
with open(visitor_filepath, 'r') as visit:
|
|
data = json.load(visit)
|
|
if data:
|
|
for element in data:
|
|
if element['user'] not in plant.visitors:
|
|
plant.visitors.append(element['user'])
|
|
if element['user'] not in visitors_this_check:
|
|
visitors_this_check.append(element['user'])
|
|
# prevent users from manually setting watered_time in the future
|
|
if int(time.time()) >= element['timestamp'] >= plant.watered_timestamp:
|
|
guest_timestamps.append(element['timestamp'])
|
|
|
|
try:
|
|
self.update_visitor_db(visitors_this_check, plant.owner)
|
|
except Exception:
|
|
pass
|
|
with open(visitor_filepath, 'w') as visitor:
|
|
visitor.write('[]')
|
|
else:
|
|
with open(visitor_filepath, 'w') as f:
|
|
json.dump([], f)
|
|
os.chmod(visitor_filepath, 0o666)
|
|
return guest_timestamps
|